Merge branch 'master' into TVFix

This commit is contained in:
cvium
2021-11-08 10:38:08 +01:00
549 changed files with 7338 additions and 4194 deletions

View File

@@ -1,13 +1,5 @@
using System;
using System.Collections.Generic;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Controllers;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Controllers
@@ -26,33 +18,28 @@ namespace Jellyfin.Api.Tests.Controllers
}
}
public static IEnumerable<object[]> GetSegmentLengths_Success_TestData()
public static TheoryData<long, int, double[]> GetSegmentLengths_Success_TestData()
{
yield return new object[] { 0, 6, Array.Empty<double>() };
yield return new object[]
{
var data = new TheoryData<long, int, double[]>();
data.Add(0, 6, Array.Empty<double>());
data.Add(
TimeSpan.FromSeconds(3).Ticks,
6,
new double[] { 3 }
};
yield return new object[]
{
new double[] { 3 });
data.Add(
TimeSpan.FromSeconds(6).Ticks,
6,
new double[] { 6 }
};
yield return new object[]
{
new double[] { 6 });
data.Add(
TimeSpan.FromSeconds(3.3333333).Ticks,
6,
new double[] { 3.3333333 }
};
yield return new object[]
{
new double[] { 3.3333333 });
data.Add(
TimeSpan.FromSeconds(9.3333333).Ticks,
6,
new double[] { 6, 3.3333333 }
};
new double[] { 6, 3.3333333 });
return data;
}
}
}

View File

@@ -15,16 +15,16 @@ namespace Jellyfin.Api.Tests.Helpers
Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder));
}
public static IEnumerable<object[]> GetOrderBy_Success_TestData()
public static TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]> GetOrderBy_Success_TestData()
{
yield return new object[]
{
var data = new TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]>();
data.Add(
Array.Empty<string>(),
Array.Empty<SortOrder>(),
Array.Empty<(string, SortOrder)>()
};
yield return new object[]
{
Array.Empty<(string, SortOrder)>());
data.Add(
new string[]
{
"IsFavoriteOrLiked",
@@ -35,10 +35,9 @@ namespace Jellyfin.Api.Tests.Helpers
{
("IsFavoriteOrLiked", SortOrder.Ascending),
("Random", SortOrder.Ascending),
}
};
yield return new object[]
{
});
data.Add(
new string[]
{
"SortName",
@@ -52,8 +51,9 @@ namespace Jellyfin.Api.Tests.Helpers
{
("SortName", SortOrder.Descending),
("ProductionYear", SortOrder.Descending),
}
};
});
return data;
}
[Fact]

View File

@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -15,9 +15,9 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.9" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-rc.2*" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0-rc.2*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />

View File

@@ -19,18 +19,16 @@ namespace Jellyfin.Common.Tests.Cryptography
Assert.Throws<ArgumentException>(() => new PasswordHash(string.Empty, Array.Empty<byte>()));
}
public static IEnumerable<object[]> Parse_Valid_TestData()
public static TheoryData<string, PasswordHash> Parse_Valid_TestData()
{
var data = new TheoryData<string, PasswordHash>();
// Id
yield return new object[]
{
data.Add(
"$PBKDF2",
new PasswordHash("PBKDF2", Array.Empty<byte>())
};
new PasswordHash("PBKDF2", Array.Empty<byte>()));
// Id + parameter
yield return new object[]
{
data.Add(
"$PBKDF2$iterations=1000",
new PasswordHash(
"PBKDF2",
@@ -39,12 +37,10 @@ namespace Jellyfin.Common.Tests.Cryptography
new Dictionary<string, string>()
{
{ "iterations", "1000" },
})
};
}));
// Id + parameters
yield return new object[]
{
data.Add(
"$PBKDF2$iterations=1000,m=120",
new PasswordHash(
"PBKDF2",
@@ -54,34 +50,28 @@ namespace Jellyfin.Common.Tests.Cryptography
{
{ "iterations", "1000" },
{ "m", "120" }
})
};
}));
// Id + hash
yield return new object[]
{
data.Add(
"$PBKDF2$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
Array.Empty<byte>(),
new Dictionary<string, string>())
};
new Dictionary<string, string>()));
// Id + salt + hash
yield return new object[]
{
data.Add(
"$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
Convert.FromHexString("69F420"),
new Dictionary<string, string>())
};
new Dictionary<string, string>()));
// Id + parameter + hash
yield return new object[]
{
data.Add(
"$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
@@ -90,12 +80,9 @@ namespace Jellyfin.Common.Tests.Cryptography
new Dictionary<string, string>()
{
{ "iterations", "1000" }
})
};
}));
// Id + parameters + hash
yield return new object[]
{
data.Add(
"$PBKDF2$iterations=1000,m=120$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
@@ -105,12 +92,9 @@ namespace Jellyfin.Common.Tests.Cryptography
{
{ "iterations", "1000" },
{ "m", "120" }
})
};
}));
// Id + parameters + salt + hash
yield return new object[]
{
data.Add(
"$PBKDF2$iterations=1000,m=120$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
@@ -120,8 +104,8 @@ namespace Jellyfin.Common.Tests.Cryptography
{
{ "iterations", "1000" },
{ "m", "120" }
})
};
}));
return data;
}
[Theory]

View File

@@ -6,17 +6,17 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
</ItemGroup>
<!-- Code Analyzers -->

View File

@@ -6,13 +6,13 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />

View File

@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />

View File

@@ -6,10 +6,17 @@ namespace Jellyfin.Extensions.Tests
{
public static class CopyToExtensionsTests
{
public static IEnumerable<object[]> CopyTo_Valid_Correct_TestData()
public static TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>> CopyTo_Valid_Correct_TestData()
{
yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 } };
yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } };
var data = new TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>>();
data.Add(
new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 });
data.Add(
new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } );
return data;
}
[Theory]
@@ -20,13 +27,26 @@ namespace Jellyfin.Extensions.Tests
Assert.Equal(expected, destination);
}
public static IEnumerable<object[]> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData()
public static TheoryData<IReadOnlyList<int>, IList<int>, int> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData()
{
yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 };
yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 };
yield return new object[] { new[] { 0, 1, 2 }, Array.Empty<int>(), 0 };
yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 };
yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 };
var data = new TheoryData<IReadOnlyList<int>, IList<int>, int>();
data.Add(
new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 );
data.Add(
new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 );
data.Add(
new[] { 0, 1, 2 }, Array.Empty<int>(), 0 );
data.Add(
new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 );
data.Add(
new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 );
return data;
}
[Theory]

View File

@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -17,7 +17,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
</ItemGroup>
<!-- Code Analyzers -->

View File

@@ -1,4 +1,3 @@
using System.Globalization;
using System.Text.Json;
using FsCheck;
using FsCheck.Xunit;

View File

@@ -5,13 +5,11 @@ namespace Jellyfin.Extensions.Tests
{
public static class ShuffleExtensionsTests
{
private static readonly Random _rng = new Random();
[Fact]
public static void Shuffle_Valid_Correct()
{
byte[] original = new byte[1 << 6];
_rng.NextBytes(original);
Random.Shared.NextBytes(original);
byte[] shuffled = (byte[])original.Clone();
shuffled.Shuffle();

View File

@@ -14,5 +14,28 @@ namespace Jellyfin.Extensions.Tests
{
Assert.Equal(count, str.AsSpan().Count(needle));
}
[Theory]
[InlineData("", 'q', "")]
[InlineData("Banana split", ' ', "Banana")]
[InlineData("Banana split", 'q', "Banana split")]
[InlineData("Banana split 2", ' ', "Banana")]
public void LeftPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult)
{
var result = str.AsSpan().LeftPart(needle).ToString();
Assert.Equal(expectedResult, result);
}
[Theory]
[InlineData("", 'q', "")]
[InlineData("Banana split", ' ', "split")]
[InlineData("Banana split", 'q', "Banana split")]
[InlineData("Banana split.", '.', "")]
[InlineData("Banana split 2", ' ', "2")]
public void RightPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult)
{
var result = str.AsSpan().RightPart(needle).ToString();
Assert.Equal(expectedResult, result);
}
}
}

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using MediaBrowser.MediaEncoding.Encoder;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
@@ -34,23 +32,21 @@ namespace Jellyfin.MediaEncoding.Tests
Assert.Equal(valid, _encoderValidator.ValidateVersionInternal(versionOutput));
}
private class GetFFmpegVersionTestData : IEnumerable<object?[]>
private class GetFFmpegVersionTestData : TheoryData<string, Version?>
{
public IEnumerator<object?[]> GetEnumerator()
public GetFFmpegVersionTestData()
{
yield return new object?[] { EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegGitUnknownOutput2, new Version(4, 0) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegGitUnknownOutput, null };
Add(EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4));
Add(EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2));
Add(EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1));
Add(EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3));
Add(EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1));
Add(EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2));
Add(EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4));
Add(EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4));
Add(EncoderValidatorTestsData.FFmpegGitUnknownOutput2, new Version(4, 0));
Add(EncoderValidatorTestsData.FFmpegGitUnknownOutput, null);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.IO;
using Xunit;
namespace Jellyfin.MediaEncoding.Tests
@@ -14,7 +15,7 @@ namespace Jellyfin.MediaEncoding.Tests
public async Task Test(string fileName)
{
var path = Path.Join("Test Data", fileName);
await using (var stream = File.OpenRead(path))
await using (var stream = AsyncFile.OpenRead(path))
{
var res = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
Assert.NotNull(res);

View File

@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -18,10 +18,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
</ItemGroup>
<!-- Code Analyzers -->

View File

@@ -4,10 +4,11 @@ using System.IO;
using System.Text.Json;
using Jellyfin.Extensions.Json;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.MediaEncoding.Tests.Probing
@@ -56,6 +57,72 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("Just color bars", res.Overview);
}
[Fact]
public void GetMediaInfo_Mp4MetaData_Success()
{
var bytes = File.ReadAllBytes("Test Data/Probing/video_mp4_metadata.json");
var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
// subtitle handling requires a localization object, set a mock to return the input string
var mockLocalization = new Mock<ILocalizationManager>();
mockLocalization.Setup(x => x.GetLocalizedString(It.IsAny<string>())).Returns<string>(x => x);
ProbeResultNormalizer localizedProbeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), mockLocalization.Object);
MediaInfo res = localizedProbeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_mp4_metadata.mkv", MediaProtocol.File);
// [Video, Audio (Main), Audio (Commentary), Subtitle (Main, Spanish), Subtitle (Main, English), Subtitle (Commentary)
Assert.Equal(6, res.MediaStreams.Count);
Assert.NotNull(res.VideoStream);
Assert.Equal(res.MediaStreams[0], res.VideoStream);
Assert.Equal(0, res.VideoStream.Index);
Assert.Equal("h264", res.VideoStream.Codec);
Assert.Equal("High", res.VideoStream.Profile);
Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
Assert.Equal(358, res.VideoStream.Height);
Assert.Equal(720, res.VideoStream.Width);
Assert.Equal("2.40:1", res.VideoStream.AspectRatio);
Assert.Equal("yuv420p", res.VideoStream.PixelFormat);
Assert.Equal(31d, res.VideoStream.Level);
Assert.Equal(1, res.VideoStream.RefFrames);
Assert.True(res.VideoStream.IsAVC);
Assert.Equal(120f, res.VideoStream.RealFrameRate);
Assert.Equal("1/90000", res.VideoStream.TimeBase);
Assert.Equal(1147365, res.VideoStream.BitRate);
Assert.Equal(8, res.VideoStream.BitDepth);
Assert.True(res.VideoStream.IsDefault);
Assert.Equal("und", res.VideoStream.Language);
Assert.Equal(MediaStreamType.Audio, res.MediaStreams[1].Type);
Assert.Equal("aac", res.MediaStreams[1].Codec);
Assert.Equal(7, res.MediaStreams[1].Channels);
Assert.True(res.MediaStreams[1].IsDefault);
Assert.Equal("eng", res.MediaStreams[1].Language);
Assert.Equal("Surround 6.1", res.MediaStreams[1].Title);
Assert.Equal(MediaStreamType.Audio, res.MediaStreams[2].Type);
Assert.Equal("aac", res.MediaStreams[2].Codec);
Assert.Equal(2, res.MediaStreams[2].Channels);
Assert.False(res.MediaStreams[2].IsDefault);
Assert.Equal("eng", res.MediaStreams[2].Language);
Assert.Equal("Commentary", res.MediaStreams[2].Title);
Assert.Equal("spa", res.MediaStreams[3].Language);
Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[3].Type);
Assert.Equal("DVDSUB", res.MediaStreams[3].Codec);
Assert.Null(res.MediaStreams[3].Title);
Assert.Equal("eng", res.MediaStreams[4].Language);
Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[4].Type);
Assert.Equal("mov_text", res.MediaStreams[4].Codec);
Assert.Null(res.MediaStreams[4].Title);
Assert.Equal("eng", res.MediaStreams[5].Language);
Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[5].Type);
Assert.Equal("mov_text", res.MediaStreams[5].Codec);
Assert.Equal("Commentary", res.MediaStreams[5].Title);
}
[Fact]
public void GetMediaInfo_MusicVideo_Success()
{
@@ -70,7 +137,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("Album", res.Album);
Assert.Equal(2021, res.ProductionYear);
Assert.True(res.PremiereDate.HasValue);
Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
}
[Fact]
@@ -86,7 +153,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("City to City", res.Album);
Assert.Equal(1978, res.ProductionYear);
Assert.True(res.PremiereDate.HasValue);
Assert.Equal(DateTime.Parse("1978-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
Assert.Equal(DateTime.Parse("1978-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
Assert.Contains("Electronic", res.Genres);
Assert.Contains("Ambient", res.Genres);
Assert.Contains("Pop", res.Genres);
@@ -106,7 +173,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("Eyes wide open", res.Album);
Assert.Equal(2020, res.ProductionYear);
Assert.True(res.PremiereDate.HasValue);
Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
Assert.Equal(22, res.People.Length);
Assert.Equal("Krysta Youngs", res.People[0].Name);
Assert.Equal(PersonType.Composer, res.People[0].Type);

View File

@@ -31,5 +31,27 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
Assert.Equal("Very good, Lieutenant.", trackEvent2.Text);
}
}
[Fact]
public void Parse_EmptyNewlineBetweenText_Success()
{
using (var stream = File.OpenRead("Test Data/example2.srt"))
{
var parsed = new SrtParser(new NullLogger<SrtParser>()).Parse(stream, CancellationToken.None);
Assert.Equal(2, parsed.TrackEvents.Count);
var trackEvent1 = parsed.TrackEvents[0];
Assert.Equal("311", trackEvent1.Id);
Assert.Equal(TimeSpan.Parse("00:16:46.465", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks);
Assert.Equal(TimeSpan.Parse("00:16:49.009", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks);
Assert.Equal("Una vez que la gente se entere" + Environment.NewLine + Environment.NewLine + "de que ustedes están aquí,", trackEvent1.Text);
var trackEvent2 = parsed.TrackEvents[1];
Assert.Equal("312", trackEvent2.Id);
Assert.Equal(TimeSpan.Parse("00:16:49.092", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks);
Assert.Equal(TimeSpan.Parse("00:16:51.470", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks);
Assert.Equal("este lugar se convertirá" + Environment.NewLine + Environment.NewLine + "en un maldito zoológico.", trackEvent2.Text);
}
}
}
}

View File

@@ -38,10 +38,11 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
}
}
public static IEnumerable<object[]> Parse_MultipleDialogues_TestData()
public static TheoryData<string, IReadOnlyList<SubtitleTrackEvent>> Parse_MultipleDialogues_TestData()
{
yield return new object[]
{
var data = new TheoryData<string, IReadOnlyList<SubtitleTrackEvent>>();
data.Add(
@"[Events]
Format: Layer, Start, End, Text
Dialogue: ,0:00:01.18,0:00:01.85,dialogue1
@@ -65,8 +66,9 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
StartPositionTicks = 31800000,
EndPositionTicks = 38500000
}
}
};
});
return data;
}
[Fact]

View File

@@ -0,0 +1,83 @@
using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using Xunit;
namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
public class SubtitleEncoderTests
{
internal static TheoryData<MediaSourceInfo, MediaStream, SubtitleEncoder.SubtitleInfo> GetReadableFile_Valid_TestData()
{
var data = new TheoryData<MediaSourceInfo, MediaStream, SubtitleEncoder.SubtitleInfo>();
data.Add(
new MediaSourceInfo()
{
Protocol = MediaProtocol.File
},
new MediaStream()
{
Path = "/media/sub.ass",
IsExternal = true
},
new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true));
data.Add(
new MediaSourceInfo()
{
Protocol = MediaProtocol.File
},
new MediaStream()
{
Path = "/media/sub.ssa",
IsExternal = true
},
new SubtitleEncoder.SubtitleInfo("/media/sub.ssa", MediaProtocol.File, "ssa", true));
data.Add(
new MediaSourceInfo()
{
Protocol = MediaProtocol.File
},
new MediaStream()
{
Path = "/media/sub.srt",
IsExternal = true
},
new SubtitleEncoder.SubtitleInfo("/media/sub.srt", MediaProtocol.File, "srt", true));
data.Add(
new MediaSourceInfo()
{
Protocol = MediaProtocol.Http
},
new MediaStream()
{
Path = "/media/sub.ass",
IsExternal = true
},
new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true));
return data;
}
[Theory]
[MemberData(nameof(GetReadableFile_Valid_TestData))]
internal async Task GetReadableFile_Valid_Success(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleEncoder.SubtitleInfo subtitleInfo)
{
var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
var subtitleEncoder = fixture.Create<SubtitleEncoder>();
var result = await subtitleEncoder.GetReadableFile(mediaSource, subtitleStream, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(subtitleInfo.Path, result.Path);
Assert.Equal(subtitleInfo.Protocol, result.Protocol);
Assert.Equal(subtitleInfo.Format, result.Format);
Assert.Equal(subtitleInfo.IsExternal, result.IsExternal);
}
}
}

View File

@@ -0,0 +1,260 @@
{
"streams": [
{
"index": 0,
"codec_name": "h264",
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "High",
"codec_type": "video",
"codec_tag_string": "avc1",
"codec_tag": "0x31637661",
"width": 720,
"height": 358,
"coded_width": 720,
"coded_height": 358,
"closed_captions": 0,
"has_b_frames": 2,
"sample_aspect_ratio": "32:27",
"display_aspect_ratio": "1280:537",
"pix_fmt": "yuv420p",
"level": 31,
"color_range": "tv",
"color_space": "smpte170m",
"color_transfer": "bt709",
"color_primaries": "smpte170m",
"chroma_location": "left",
"refs": 1,
"is_avc": "true",
"nal_length_size": "4",
"r_frame_rate": "120/1",
"avg_frame_rate": "1704753000/71073479",
"time_base": "1/90000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 1421469580,
"duration": "15794.106444",
"bit_rate": "1147365",
"bits_per_raw_sample": "8",
"nb_frames": "378834",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
},
"tags": {
"creation_time": "2021-09-13T22:42:42.000000Z",
"language": "und",
"handler_name": "VideoHandler",
"vendor_id": "[0][0][0][0]"
}
},
{
"index": 1,
"codec_name": "aac",
"codec_long_name": "AAC (Advanced Audio Coding)",
"profile": "LC",
"codec_type": "audio",
"codec_tag_string": "mp4a",
"codec_tag": "0x6134706d",
"sample_fmt": "fltp",
"sample_rate": "48000",
"channels": 7,
"bits_per_sample": 0,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 758115312,
"duration": "15794.069000",
"bit_rate": "224197",
"nb_frames": "740348",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
},
"tags": {
"creation_time": "2021-09-13T22:42:42.000000Z",
"language": "eng",
"handler_name": "Surround 6.1",
"vendor_id": "[0][0][0][0]"
}
},
{
"index": 2,
"codec_name": "aac",
"codec_long_name": "AAC (Advanced Audio Coding)",
"profile": "LC",
"codec_type": "audio",
"codec_tag_string": "mp4a",
"codec_tag": "0x6134706d",
"sample_fmt": "fltp",
"sample_rate": "48000",
"channels": 2,
"channel_layout": "stereo",
"bits_per_sample": 0,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 758114304,
"duration": "15794.048000",
"bit_rate": "160519",
"nb_frames": "740347",
"disposition": {
"default": 0,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
},
"tags": {
"creation_time": "2021-09-13T22:42:42.000000Z",
"language": "eng",
"handler_name": "Commentary",
"vendor_id": "[0][0][0][0]"
}
},
{
"index": 3,
"codec_name": "dvd_subtitle",
"codec_long_name": "DVD subtitles",
"codec_type": "subtitle",
"codec_tag_string": "mp4s",
"codec_tag": "0x7334706d",
"width": 720,
"height": 480,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/90000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 1300301588,
"duration": "14447.795422",
"bit_rate": "2653",
"nb_frames": "3545",
"disposition": {
"default": 0,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
},
"tags": {
"creation_time": "2021-09-13T22:42:42.000000Z",
"language": "spa",
"handler_name": "SubtitleHandler"
}
},
{
"index": 4,
"codec_name": "mov_text",
"codec_long_name": "MOV text",
"codec_type": "subtitle",
"codec_tag_string": "tx3g",
"codec_tag": "0x67337874",
"width": 853,
"height": 51,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/90000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 1401339330,
"duration": "15570.437000",
"bit_rate": "88",
"nb_frames": "5079",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
},
"tags": {
"creation_time": "2021-09-13T22:42:42.000000Z",
"language": "eng",
"handler_name": "SubtitleHandler"
}
},
{
"index": 5,
"codec_name": "mov_text",
"codec_long_name": "MOV text",
"codec_type": "subtitle",
"codec_tag_string": "tx3g",
"codec_tag": "0x67337874",
"width": 853,
"height": 51,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/90000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 1370580300,
"duration": "15228.670000",
"bit_rate": "18",
"nb_frames": "1563",
"disposition": {
"default": 0,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
},
"tags": {
"creation_time": "2021-09-13T22:42:42.000000Z",
"language": "eng",
"handler_name": "Commentary"
}
}
]
}

View File

@@ -0,0 +1,11 @@
311
00:16:46,465 --> 00:16:49,009
Una vez que la gente se entere
de que ustedes están aquí,
312
00:16:49,092 --> 00:16:51,470
este lugar se convertirá
en un maldito zoológico.

View File

@@ -1,4 +1,3 @@
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
using Xunit;
@@ -6,12 +5,11 @@ namespace Jellyfin.Model.Tests.Entities
{
public class MediaStreamTests
{
public static IEnumerable<object[]> Get_DisplayTitle_TestData()
public static TheoryData<MediaStream, string> Get_DisplayTitle_TestData()
{
return new List<object[]>
{
new object[]
{
var data = new TheoryData<MediaStream, string>();
data.Add(
new MediaStream
{
Type = MediaStreamType.Subtitle,
@@ -21,61 +19,57 @@ namespace Jellyfin.Model.Tests.Entities
IsDefault = false,
Codec = "ASS"
},
"English - Und - ASS"
},
new object[]
"English - Und - ASS");
data.Add(
new MediaStream
{
new MediaStream
{
Type = MediaStreamType.Subtitle,
Title = "English",
Language = string.Empty,
IsForced = false,
IsDefault = false,
Codec = string.Empty
},
"English - Und"
Type = MediaStreamType.Subtitle,
Title = "English",
Language = string.Empty,
IsForced = false,
IsDefault = false,
Codec = string.Empty
},
new object[]
"English - Und");
data.Add(
new MediaStream
{
new MediaStream
{
Type = MediaStreamType.Subtitle,
Title = "English",
Language = "EN",
IsForced = false,
IsDefault = false,
Codec = string.Empty
},
"English"
Type = MediaStreamType.Subtitle,
Title = "English",
Language = "EN",
IsForced = false,
IsDefault = false,
Codec = string.Empty
},
new object[]
"English");
data.Add(
new MediaStream
{
new MediaStream
{
Type = MediaStreamType.Subtitle,
Title = "English",
Language = "EN",
IsForced = true,
IsDefault = true,
Codec = "SRT"
},
"English - Default - Forced - SRT"
Type = MediaStreamType.Subtitle,
Title = "English",
Language = "EN",
IsForced = true,
IsDefault = true,
Codec = "SRT"
},
new object[]
"English - Default - Forced - SRT");
data.Add(
new MediaStream
{
new MediaStream
{
Type = MediaStreamType.Subtitle,
Title = null,
Language = null,
IsForced = false,
IsDefault = false,
Codec = null
},
"Und"
}
};
Type = MediaStreamType.Subtitle,
Title = null,
Language = null,
IsForced = false,
IsDefault = false,
Codec = null
},
"Und");
return data;
}
[Theory]

View File

@@ -1,17 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
</ItemGroup>
<!-- Code Analyzers -->

View File

@@ -1,4 +1,3 @@
using System.Collections.Generic;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using Xunit;
@@ -9,29 +8,29 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
private readonly NamingOptions _namingOptions = new NamingOptions();
public static IEnumerable<object[]> Resolve_ValidFileNameTestData()
public static TheoryData<AudioBookFileInfo> Resolve_ValidFileNameTestData()
{
yield return new object[]
{
var data = new TheoryData<AudioBookFileInfo>();
data.Add(
new AudioBookFileInfo(
@"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
"mp3")
};
yield return new object[]
{
"mp3"));
data.Add(
new AudioBookFileInfo(
@"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
"ogg",
chapterNumber: 1)
};
yield return new object[]
{
chapterNumber: 1));
data.Add(
new AudioBookFileInfo(
@"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
"mp3",
chapterNumber: 2,
partNumber: 3)
};
partNumber: 3));
return data;
}
[Theory]

View File

@@ -6,13 +6,13 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />

View File

@@ -71,9 +71,9 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("Season 1/seriesname 05.mkv", 5)] // no hyphen between series name and episode number
[InlineData("[BBT-RMX] Ranma ½ - 154 [50AC421A].mkv", 154)] // hyphens in the pre-name info, triple digit episode number
[InlineData("Season 2/Episode 21 - 94 Meetings.mp4", 21)] // Title starts with a number
[InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)]
// [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number
// TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)]
// TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)]
// TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)]
// TODO: [InlineData("Season 2/7 12 Angry Men.avi", 7)]
// TODO: [InlineData("Season 02/02x03x04x15 - Ep Name.mp4", 2)]

View File

@@ -0,0 +1,28 @@
using Emby.Naming.Common;
using Emby.Naming.TV;
using Xunit;
namespace Jellyfin.Naming.Tests.TV
{
public class SeriesPathParserTest
{
[Theory]
[InlineData("The.Show.S01", "The.Show")]
[InlineData("/The.Show.S01", "The.Show")]
[InlineData("/some/place/The.Show.S01", "The.Show")]
[InlineData("/something/The.Show.S01", "The.Show")]
[InlineData("The Show Season 10", "The Show")]
[InlineData("The Show S01E01", "The Show")]
[InlineData("The Show S01E01 Episode", "The Show")]
[InlineData("/something/The Show/Season 1", "The Show")]
[InlineData("/something/The Show/S01", "The Show")]
public void SeriesPathParserParseTest(string path, string name)
{
NamingOptions o = new NamingOptions();
var res = SeriesPathParser.Parse(o, path);
Assert.Equal(name, res.SeriesName);
Assert.True(res.Success);
}
}
}

View File

@@ -0,0 +1,28 @@
using Emby.Naming.Common;
using Emby.Naming.TV;
using Xunit;
namespace Jellyfin.Naming.Tests.TV
{
public class SeriesResolverTests
{
[Theory]
[InlineData("The.Show.S01", "The Show")]
[InlineData("The.Show.S01.COMPLETE", "The Show")]
[InlineData("S.H.O.W.S01", "S.H.O.W")]
[InlineData("The.Show.P.I.S01", "The Show P.I")]
[InlineData("The_Show_Season_1", "The Show")]
[InlineData("/something/The_Show/Season 10", "The Show")]
[InlineData("The Show", "The Show")]
[InlineData("/some/path/The Show", "The Show")]
[InlineData("/some/path/The Show s02e10 720p hdtv", "The Show")]
[InlineData("/some/path/The Show s02e10 the episode 720p hdtv", "The Show")]
public void SeriesResolverResolveTest(string path, string name)
{
NamingOptions o = new NamingOptions();
var res = SeriesResolver.Resolve(o, path);
Assert.Equal(name, res.Name);
}
}
}

View File

@@ -1,4 +1,3 @@
using System;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Xunit;
@@ -23,12 +22,17 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("Crouching.Tiger.Hidden.Dragon.BDrip.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.4K.UltraHD.HDR.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("[HorribleSubs] Made in Abyss - 13 [720p].mkv", "Made in Abyss")]
[InlineData("[Tsundere] Kore wa Zombie Desu ka of the Dead [BDRip h264 1920x1080 FLAC]", "Kore wa Zombie Desu ka of the Dead")]
[InlineData("[Erai-raws] Jujutsu Kaisen - 03 [720p][Multiple Subtitle].mkv", "Jujutsu Kaisen")]
[InlineData("[OCN] 720p-NEXT", " ")]
[InlineData("[tvN] .E01-E16.720p-NEXT", "")]
[InlineData("[tvN] E01~E16 END HDTV.H264.720p-WITH", " ")]
// FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName)
{
Assert.True(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
// TODO: compare spans when XUnit supports it
Assert.Equal(expectedName, newName.ToString());
Assert.True(VideoResolver.TryCleanString(input, _namingOptions, out var newName));
Assert.Equal(expectedName, newName);
}
[Theory]
@@ -41,8 +45,8 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("Run lola run (lola rennt) (2009).mp4")]
public void CleanStringTest_DoesntNeedCleaning_False(string? input)
{
Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
Assert.True(newName.IsEmpty);
Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out var newName));
Assert.True(string.IsNullOrEmpty(newName));
}
}
}

View File

@@ -18,30 +18,31 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestKodiExtras()
{
Test("trailer.mp4", ExtraType.Trailer, _videoOptions);
Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions);
Test("trailer.mp4", ExtraType.Trailer);
Test("300-trailer.mp4", ExtraType.Trailer);
Test("theme.mp3", ExtraType.ThemeSong, _videoOptions);
Test("theme.mp3", ExtraType.ThemeSong);
}
[Fact]
public void TestExpandedExtras()
{
Test("trailer.mp4", ExtraType.Trailer, _videoOptions);
Test("trailer.mp3", null, _videoOptions);
Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions);
Test("trailer.mp4", ExtraType.Trailer);
Test("trailer.mp3", null);
Test("300-trailer.mp4", ExtraType.Trailer);
Test("stuff trailerthings.mkv", null);
Test("theme.mp3", ExtraType.ThemeSong, _videoOptions);
Test("theme.mkv", null, _videoOptions);
Test("theme.mp3", ExtraType.ThemeSong);
Test("theme.mkv", null);
Test("300-scene.mp4", ExtraType.Scene, _videoOptions);
Test("300-scene2.mp4", ExtraType.Scene, _videoOptions);
Test("300-clip.mp4", ExtraType.Clip, _videoOptions);
Test("300-scene.mp4", ExtraType.Scene);
Test("300-scene2.mp4", ExtraType.Scene);
Test("300-clip.mp4", ExtraType.Clip);
Test("300-deleted.mp4", ExtraType.DeletedScene, _videoOptions);
Test("300-deletedscene.mp4", ExtraType.DeletedScene, _videoOptions);
Test("300-interview.mp4", ExtraType.Interview, _videoOptions);
Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, _videoOptions);
Test("300-deleted.mp4", ExtraType.DeletedScene);
Test("300-deletedscene.mp4", ExtraType.DeletedScene);
Test("300-interview.mp4", ExtraType.Interview);
Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes);
}
[Theory]
@@ -55,9 +56,9 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData(ExtraType.Unknown, "extras")]
public void TestDirectories(ExtraType type, string dirName)
{
Test(dirName + "/300.mp4", type, _videoOptions);
Test("300/" + dirName + "/something.mkv", type, _videoOptions);
Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", type, _videoOptions);
Test(dirName + "/300.mp4", type);
Test("300/" + dirName + "/something.mkv", type);
Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", type);
}
[Theory]
@@ -66,32 +67,25 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("The Big Short")]
public void TestNonExtraDirectories(string dirName)
{
Test(dirName + "/300.mp4", null, _videoOptions);
Test("300/" + dirName + "/something.mkv", null, _videoOptions);
Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", null, _videoOptions);
Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null, _videoOptions);
Test(dirName + "/300.mp4", null);
Test("300/" + dirName + "/something.mkv", null);
Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", null);
Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null);
}
[Fact]
public void TestSample()
{
Test("300-sample.mp4", ExtraType.Sample, _videoOptions);
Test("300-sample.mp4", ExtraType.Sample);
}
private void Test(string input, ExtraType? expectedType, NamingOptions videoOptions)
private void Test(string input, ExtraType? expectedType)
{
var parser = GetExtraTypeParser(videoOptions);
var parser = GetExtraTypeParser(_videoOptions);
var extraType = parser.GetExtraInfo(input).ExtraType;
if (expectedType == null)
{
Assert.Null(extraType);
}
else
{
Assert.Equal(expectedType, extraType);
}
Assert.Equal(expectedType, extraType);
}
[Fact]

View File

@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
@@ -11,148 +10,134 @@ namespace Jellyfin.Naming.Tests.Video
{
private static NamingOptions _namingOptions = new NamingOptions();
public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData()
public static TheoryData<VideoFileInfo> ResolveFile_ValidFileNameTestData()
{
yield return new object[]
{
var data = new TheoryData<VideoFileInfo>();
data.Add(
new VideoFileInfo(
path: @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
container: "mkv",
name: "7 Psychos")
};
yield return new object[]
{
name: "7 Psychos"));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
container: "mkv",
name: "3 days to kill",
year: 2005)
};
yield return new object[]
{
year: 2005));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/American Psycho/American.Psycho.mkv",
container: "mkv",
name: "American.Psycho")
};
yield return new object[]
{
name: "American.Psycho"));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
container: "mkv",
name: "brave",
year: 2006,
is3D: true,
format3D: "sbs")
};
yield return new object[]
{
format3D: "sbs"));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
container: "mkv",
name: "300",
year: 2006)
};
yield return new object[]
{
year: 2006));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
container: "mkv",
name: "300",
year: 2006,
is3D: true,
format3D: "sbs")
};
yield return new object[]
{
format3D: "sbs"));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
container: "disc",
name: "brave",
year: 2006,
isStub: true,
stubType: "bluray")
};
yield return new object[]
{
stubType: "bluray"));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
container: "disc",
name: "300",
year: 2006,
isStub: true,
stubType: "bluray")
};
yield return new object[]
{
stubType: "bluray"));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
container: "disc",
name: "Brave",
year: 2006,
isStub: true,
stubType: "bluray")
};
yield return new object[]
{
stubType: "bluray"));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).bluray.disc",
container: "disc",
name: "300",
year: 2006,
isStub: true,
stubType: "bluray")
};
yield return new object[]
{
stubType: "bluray"));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
container: "mkv",
name: "300",
year: 2006,
extraType: ExtraType.Trailer)
};
yield return new object[]
{
extraType: ExtraType.Trailer));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
container: "mkv",
name: "Brave",
year: 2006,
extraType: ExtraType.Trailer)
};
yield return new object[]
{
extraType: ExtraType.Trailer));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).mkv",
container: "mkv",
name: "300",
year: 2006)
};
yield return new object[]
{
year: 2006));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
container: "mkv",
name: "Bad Boys",
year: 1995)
};
yield return new object[]
{
year: 1995));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006).mkv",
container: "mkv",
name: "Brave",
year: 2006)
};
yield return new object[]
{
year: 2006));
data.Add(
new VideoFileInfo(
path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF.mp4",
container: "mp4",
name: "Rain Man",
year: 1988)
};
year: 1988));
return data;
}
[Theory]

View File

@@ -6,17 +6,17 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>

View File

@@ -20,7 +20,7 @@ namespace Jellyfin.Networking.Tests
CallBase = true
};
configManager.Setup(x => x.GetConfiguration(It.IsAny<string>())).Returns(conf);
return (IConfigurationManager)configManager.Object;
return configManager.Object;
}
/// <summary>

View File

@@ -1,13 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<None Include="Test Data\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">

View File

@@ -0,0 +1,597 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Providers;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Providers.Tests.Manager
{
public class ItemImageProviderTests
{
private const string TestDataImagePath = "Test Data/Images/blank{0}.jpg";
[Fact]
public void ValidateImages_PhotoEmptyProviders_NoChange()
{
var itemImageProvider = GetItemImageProvider(null, null);
var changed = itemImageProvider.ValidateImages(new Photo(), Enumerable.Empty<ILocalImageProvider>(), null);
Assert.False(changed);
}
[Fact]
public void ValidateImages_EmptyItemEmptyProviders_NoChange()
{
var itemImageProvider = GetItemImageProvider(null, null);
var changed = itemImageProvider.ValidateImages(new Video(), Enumerable.Empty<ILocalImageProvider>(), null);
Assert.False(changed);
}
private static TheoryData<ImageType, int> GetImageTypesWithCount()
{
var theoryTypes = new TheoryData<ImageType, int>
{
// minimal test cases that hit different handling
{ ImageType.Primary, 1 },
{ ImageType.Backdrop, 1 },
{ ImageType.Backdrop, 2 }
};
return theoryTypes;
}
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public void ValidateImages_EmptyItemAndPopulatedProviders_AddsImages(ImageType imageType, int imageCount)
{
// Has to exist for querying DateModified time on file, results stored but not checked so not populating
BaseItem.FileSystem = Mock.Of<IFileSystem>();
var item = new Video();
var imageProvider = GetImageProvider(imageType, imageCount, true);
var itemImageProvider = GetItemImageProvider(null, null);
var changed = itemImageProvider.ValidateImages(item, new[] { imageProvider }, null);
Assert.True(changed);
Assert.Equal(imageCount, item.GetImages(imageType).Count());
}
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public void ValidateImages_PopulatedItemWithGoodPathsAndEmptyProviders_NoChange(ImageType imageType, int imageCount)
{
var item = GetItemWithImages(imageType, imageCount, true);
var itemImageProvider = GetItemImageProvider(null, null);
var changed = itemImageProvider.ValidateImages(item, Enumerable.Empty<ILocalImageProvider>(), null);
Assert.False(changed);
Assert.Equal(imageCount, item.GetImages(imageType).Count());
}
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public void ValidateImages_PopulatedItemWithBadPathsAndEmptyProviders_RemovesImage(ImageType imageType, int imageCount)
{
var item = GetItemWithImages(imageType, imageCount, false);
var itemImageProvider = GetItemImageProvider(null, null);
var changed = itemImageProvider.ValidateImages(item, Enumerable.Empty<ILocalImageProvider>(), null);
Assert.True(changed);
Assert.Empty(item.GetImages(imageType));
}
[Fact]
public void MergeImages_EmptyItemNewImagesEmpty_NoChange()
{
var itemImageProvider = GetItemImageProvider(null, null);
var changed = itemImageProvider.MergeImages(new Video(), Array.Empty<LocalImageInfo>());
Assert.False(changed);
}
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public void MergeImages_PopulatedItemWithGoodPathsAndPopulatedNewImages_AddsUpdatesImages(ImageType imageType, int imageCount)
{
// valid and not valid paths - should replace the valid paths with the invalid ones
var item = GetItemWithImages(imageType, imageCount, true);
var images = GetImages(imageType, imageCount, false);
var itemImageProvider = GetItemImageProvider(null, null);
var changed = itemImageProvider.MergeImages(item, images);
Assert.True(changed);
// adds for types that allow multiple, replaces singular type images
if (item.AllowsMultipleImages(imageType))
{
Assert.Equal(imageCount * 2, item.GetImages(imageType).Count());
}
else
{
Assert.Single(item.GetImages(imageType));
Assert.Same(images[0].FileInfo.FullName, item.GetImages(imageType).First().Path);
}
}
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public void MergeImages_PopulatedItemWithGoodPathsAndSameNewImages_NoChange(ImageType imageType, int imageCount)
{
var oldTime = new DateTime(1970, 1, 1);
// match update time with time added to item images (unix epoch)
var fileSystem = new Mock<IFileSystem>();
fileSystem.Setup(fs => fs.GetLastWriteTimeUtc(It.IsAny<FileSystemMetadata>()))
.Returns(oldTime);
BaseItem.FileSystem = fileSystem.Object;
// all valid paths - matching for strictly updating
var item = GetItemWithImages(imageType, imageCount, true);
// set size to non-zero to allow for updates to occur
foreach (var image in item.GetImages(imageType))
{
image.DateModified = oldTime;
image.Height = 1;
image.Width = 1;
}
var images = GetImages(imageType, imageCount, true);
var itemImageProvider = GetItemImageProvider(null, fileSystem);
var changed = itemImageProvider.MergeImages(item, images);
Assert.False(changed);
}
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public void MergeImages_PopulatedItemWithGoodPathsAndSameNewImagesWithNewTimestamps_ResetsImageSizes(ImageType imageType, int imageCount)
{
var oldTime = new DateTime(1970, 1, 1);
var updatedTime = new DateTime(2021, 1, 1);
var fileSystem = new Mock<IFileSystem>();
fileSystem.Setup(fs => fs.GetLastWriteTimeUtc(It.IsAny<FileSystemMetadata>()))
.Returns(updatedTime);
BaseItem.FileSystem = fileSystem.Object;
// all valid paths - matching for strictly updating
var item = GetItemWithImages(imageType, imageCount, true);
// set size to non-zero to allow for image size reset to occur
foreach (var image in item.GetImages(imageType))
{
image.DateModified = oldTime;
image.Height = 1;
image.Width = 1;
}
var images = GetImages(imageType, imageCount, true);
var itemImageProvider = GetItemImageProvider(null, fileSystem);
var changed = itemImageProvider.MergeImages(item, images);
Assert.True(changed);
// before and after paths are the same, verify updated by size reset to 0
Assert.Equal(imageCount, item.GetImages(imageType).Count());
foreach (var image in item.GetImages(imageType))
{
Assert.Equal(updatedTime, image.DateModified);
Assert.Equal(0, image.Height);
Assert.Equal(0, image.Width);
}
}
[Theory]
[InlineData(ImageType.Primary, 1, false)]
[InlineData(ImageType.Backdrop, 2, false)]
[InlineData(ImageType.Primary, 1, true)]
[InlineData(ImageType.Backdrop, 2, true)]
public async void RefreshImages_PopulatedItemPopulatedProviderDynamic_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
{
var item = GetItemWithImages(imageType, imageCount, false);
var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
var imageResponse = new DynamicImageResponse
{
HasImage = true,
Format = ImageFormat.Jpg,
Path = "url path",
Protocol = MediaProtocol.Http
};
var dynamicProvider = new Mock<IDynamicImageProvider>(MockBehavior.Strict);
dynamicProvider.Setup(rp => rp.Name).Returns("MockDynamicProvider");
dynamicProvider.Setup(rp => rp.GetSupportedImages(item))
.Returns(new[] { imageType });
dynamicProvider.Setup(rp => rp.GetImage(item, imageType, It.IsAny<CancellationToken>()))
.ReturnsAsync(imageResponse);
var refreshOptions = forceRefresh
? new ImageRefreshOptions(null)
{
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
ReplaceAllImages = true
}
: new ImageRefreshOptions(null);
var itemImageProvider = GetItemImageProvider(null, new Mock<IFileSystem>());
var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None);
Assert.Equal(forceRefresh, result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
if (forceRefresh)
{
// replaces multi-types
Assert.Single(item.GetImages(imageType));
}
else
{
// adds to multi-types if room
Assert.Equal(imageCount, item.GetImages(imageType).Count());
}
}
[Theory]
[InlineData(ImageType.Primary, 1, true, MediaProtocol.Http)]
[InlineData(ImageType.Backdrop, 2, true, MediaProtocol.Http)]
[InlineData(ImageType.Primary, 1, true, MediaProtocol.File)]
[InlineData(ImageType.Backdrop, 2, true, MediaProtocol.File)]
[InlineData(ImageType.Primary, 1, false, MediaProtocol.File)]
[InlineData(ImageType.Backdrop, 2, false, MediaProtocol.File)]
public async void RefreshImages_EmptyItemPopulatedProviderDynamic_AddsImages(ImageType imageType, int imageCount, bool responseHasPath, MediaProtocol protocol)
{
// Has to exist for querying DateModified time on file, results stored but not checked so not populating
BaseItem.FileSystem = Mock.Of<IFileSystem>();
var item = new Video();
var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
// Path must exist if set: is read in as a stream by AsyncFile.OpenRead
var imageResponse = new DynamicImageResponse
{
HasImage = true,
Format = ImageFormat.Jpg,
Path = responseHasPath ? string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0) : null,
Protocol = protocol
};
var dynamicProvider = new Mock<IDynamicImageProvider>(MockBehavior.Strict);
dynamicProvider.Setup(rp => rp.Name).Returns("MockDynamicProvider");
dynamicProvider.Setup(rp => rp.GetSupportedImages(item))
.Returns(new[] { imageType });
dynamicProvider.Setup(rp => rp.GetImage(item, imageType, It.IsAny<CancellationToken>()))
.ReturnsAsync(imageResponse);
var refreshOptions = new ImageRefreshOptions(null);
var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<Stream>(), It.IsAny<string>(), imageType, null, It.IsAny<CancellationToken>()))
.Callback<BaseItem, Stream, string, ImageType, int?, CancellationToken>((callbackItem, _, _, callbackType, _, _) => callbackItem.SetImagePath(callbackType, 0, new FileSystemMetadata()))
.Returns(Task.CompletedTask);
var itemImageProvider = GetItemImageProvider(providerManager.Object, null);
var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None);
Assert.True(result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
// dynamic provider unable to return multiple images
Assert.Single(item.GetImages(imageType));
if (protocol == MediaProtocol.Http)
{
Assert.Equal(imageResponse.Path, item.GetImagePath(imageType, 0));
}
}
[Theory]
[InlineData(ImageType.Primary, 1, false)]
[InlineData(ImageType.Backdrop, 1, false)]
[InlineData(ImageType.Backdrop, 2, false)]
[InlineData(ImageType.Primary, 1, true)]
[InlineData(ImageType.Backdrop, 1, true)]
[InlineData(ImageType.Backdrop, 2, true)]
public async void RefreshImages_PopulatedItemPopulatedProviderRemote_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
{
var item = GetItemWithImages(imageType, imageCount, false);
var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
remoteProvider.Setup(rp => rp.GetSupportedImages(item))
.Returns(new[] { imageType });
var refreshOptions = forceRefresh
? new ImageRefreshOptions(null)
{
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
ReplaceAllImages = true
}
: new ImageRefreshOptions(null);
var remoteInfo = new RemoteImageInfo[imageCount];
for (int i = 0; i < imageCount; i++)
{
remoteInfo[i] = new RemoteImageInfo
{
Type = imageType,
Url = "image url " + i,
Width = 1 // min width is set to 0, this will always pass
};
}
var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
providerManager.Setup(pm => pm.GetAvailableRemoteImages(It.IsAny<BaseItem>(), It.IsAny<RemoteImageQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(remoteInfo);
var itemImageProvider = GetItemImageProvider(providerManager.Object, new Mock<IFileSystem>());
var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
Assert.Equal(forceRefresh, result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
Assert.Equal(imageCount, item.GetImages(imageType).Count());
foreach (var image in item.GetImages(imageType))
{
if (forceRefresh)
{
Assert.Matches(@"image url [0-9]", image.Path);
}
else
{
Assert.DoesNotMatch(@"image url [0-9]", image.Path);
}
}
}
[Theory]
[InlineData(ImageType.Primary, 0, false)] // singular type only fetches if type is missing from item, no caching
[InlineData(ImageType.Backdrop, 0, false)] // empty item, no cache to check
[InlineData(ImageType.Backdrop, 1, false)] // populated item, cached so no download
[InlineData(ImageType.Backdrop, 1, true)] // populated item, forced to download
public async void RefreshImages_NonStubItemPopulatedProviderRemote_DownloadsIfNecessary(ImageType imageType, int initialImageCount, bool fullRefresh)
{
var targetImageCount = 1;
// Set path and media source manager so images will be downloaded (EnableImageStub will return false)
var item = GetItemWithImages(imageType, initialImageCount, false);
item.Path = "non-empty path";
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
// seek 2 so it won't short-circuit out of downloading when populated
var libraryOptions = GetLibraryOptions(item, imageType, 2);
const string Content = "Content";
var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
remoteProvider.Setup(rp => rp.GetSupportedImages(item))
.Returns(new[] { imageType });
remoteProvider.Setup(rp => rp.GetImageResponse(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((string url, CancellationToken _) => new HttpResponseMessage
{
ReasonPhrase = url,
StatusCode = HttpStatusCode.OK,
Content = new StringContent(Content, Encoding.UTF8, "image/jpeg")
});
var refreshOptions = fullRefresh
? new ImageRefreshOptions(null)
{
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
ReplaceAllImages = true
}
: new ImageRefreshOptions(null);
var remoteInfo = new RemoteImageInfo[targetImageCount];
for (int i = 0; i < targetImageCount; i++)
{
remoteInfo[i] = new RemoteImageInfo()
{
Type = imageType,
Url = "image url " + i,
Width = 1 // min width is set to 0, this will always pass
};
}
var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
providerManager.Setup(pm => pm.GetAvailableRemoteImages(It.IsAny<BaseItem>(), It.IsAny<RemoteImageQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(remoteInfo);
providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<Stream>(), It.IsAny<string>(), imageType, null, It.IsAny<CancellationToken>()))
.Callback<BaseItem, Stream, string, ImageType, int?, CancellationToken>((callbackItem, _, _, callbackType, _, _) =>
callbackItem.SetImagePath(callbackType, callbackItem.AllowsMultipleImages(callbackType) ? callbackItem.GetImages(callbackType).Count() : 0, new FileSystemMetadata()))
.Returns(Task.CompletedTask);
var fileSystem = new Mock<IFileSystem>();
// match reported file size to image content length - condition for skipping already downloaded multi-images
fileSystem.Setup(fs => fs.GetFileInfo(It.IsAny<string>()))
.Returns(new FileSystemMetadata { Length = Content.Length });
var itemImageProvider = GetItemImageProvider(providerManager.Object, fileSystem);
var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
Assert.Equal(initialImageCount == 0 || fullRefresh, result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
Assert.Equal(targetImageCount, item.GetImages(imageType).Count());
}
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public async void RefreshImages_EmptyItemPopulatedProviderRemoteExtras_LimitsImages(ImageType imageType, int imageCount)
{
var item = new Video();
var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
remoteProvider.Setup(rp => rp.GetSupportedImages(item))
.Returns(new[] { imageType });
var refreshOptions = new ImageRefreshOptions(null);
// populate remote with double the required images to verify count is trimmed to the library option count
var remoteInfoCount = imageCount * 2;
var remoteInfo = new RemoteImageInfo[remoteInfoCount];
for (int i = 0; i < remoteInfoCount; i++)
{
remoteInfo[i] = new RemoteImageInfo()
{
Type = imageType,
Url = "image url " + i,
Width = 1 // min width is set to 0, this will always pass
};
}
var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
providerManager.Setup(pm => pm.GetAvailableRemoteImages(It.IsAny<BaseItem>(), It.IsAny<RemoteImageQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(remoteInfo);
var itemImageProvider = GetItemImageProvider(providerManager.Object, null);
var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
Assert.True(result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
var actualImages = item.GetImages(imageType).ToList();
Assert.Equal(imageCount, actualImages.Count);
// images from the provider manager are sorted by preference (earlier images are higher priority) so we can verify that low url numbers are chosen
foreach (var image in actualImages)
{
var index = int.Parse(Regex.Match(image.Path, @"[0-9]+").Value, NumberStyles.Integer, CultureInfo.InvariantCulture);
Assert.True(index < imageCount);
}
}
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public async void RefreshImages_PopulatedItemEmptyProviderRemoteFullRefresh_DoesntClearImages(ImageType imageType, int imageCount)
{
var item = GetItemWithImages(imageType, imageCount, false);
var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
remoteProvider.Setup(rp => rp.GetSupportedImages(item))
.Returns(new[] { imageType });
var refreshOptions = new ImageRefreshOptions(null)
{
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
ReplaceAllImages = true
};
var itemImageProvider = GetItemImageProvider(Mock.Of<IProviderManager>(), null);
var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
Assert.False(result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
Assert.Equal(imageCount, item.GetImages(imageType).Count());
}
private static ItemImageProvider GetItemImageProvider(IProviderManager? providerManager, Mock<IFileSystem>? mockFileSystem)
{
// strict to ensure this isn't accidentally used where a prepared mock is intended
providerManager ??= Mock.Of<IProviderManager>(MockBehavior.Strict);
// BaseItem.ValidateImages depends on the directory service being able to list directory contents, give it the expected valid file paths
mockFileSystem ??= new Mock<IFileSystem>(MockBehavior.Strict);
mockFileSystem.Setup(fs => fs.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>()))
.Returns(new[]
{
string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0),
string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 1)
});
return new ItemImageProvider(new NullLogger<ItemImageProvider>(), providerManager, mockFileSystem.Object);
}
private static BaseItem GetItemWithImages(ImageType type, int count, bool validPaths)
{
// Has to exist for querying DateModified time on file, results stored but not checked so not populating
BaseItem.FileSystem ??= Mock.Of<IFileSystem>();
var item = new Video();
var path = validPaths ? TestDataImagePath : "invalid path {0}";
for (int i = 0; i < count; i++)
{
item.SetImagePath(type, i, new FileSystemMetadata
{
FullName = string.Format(CultureInfo.InvariantCulture, path, i),
});
}
return item;
}
private static ILocalImageProvider GetImageProvider(ImageType type, int count, bool validPaths)
{
var images = GetImages(type, count, validPaths);
var imageProvider = new Mock<ILocalImageProvider>();
imageProvider.Setup(ip => ip.GetImages(It.IsAny<BaseItem>(), It.IsAny<IDirectoryService>()))
.Returns(images);
return imageProvider.Object;
}
/// <summary>
/// Creates a list of <see cref="LocalImageInfo"/> references of the specified type and size, optionally pointing to files that exist.
/// </summary>
private static LocalImageInfo[] GetImages(ImageType type, int count, bool validPaths)
{
var path = validPaths ? TestDataImagePath : "invalid path {0}";
var images = new LocalImageInfo[count];
for (int i = 0; i < count; i++)
{
images[i] = new LocalImageInfo
{
Type = type,
FileInfo = new FileSystemMetadata
{
FullName = string.Format(CultureInfo.InvariantCulture, path, i)
}
};
}
return images;
}
/// <summary>
/// Generates a <see cref="LibraryOptions"/> object that will allow for the requested number of images for the target type.
/// </summary>
private static LibraryOptions GetLibraryOptions(BaseItem item, ImageType type, int count)
{
return new LibraryOptions
{
TypeOptions = new[]
{
new TypeOptions
{
Type = item.GetType().Name,
ImageOptions = new[]
{
new ImageOption
{
Type = type,
Limit = count,
MinWidth = 0
}
}
}
}
};
}
}
}

View File

@@ -0,0 +1,216 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Providers.MediaInfo;
using Moq;
using Xunit;
namespace Jellyfin.Providers.Tests.MediaInfo
{
public class EmbeddedImageProviderTests
{
private static TheoryData<BaseItem> GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData()
{
return new ()
{
new AudioBook(),
new BoxSet(),
new Series(),
new Season(),
};
}
[Theory]
[MemberData(nameof(GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData))]
public void GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty(BaseItem item)
{
var embeddedImageProvider = GetEmbeddedImageProvider(null);
Assert.Empty(embeddedImageProvider.GetSupportedImages(item));
}
private static TheoryData<BaseItem, IEnumerable<ImageType>> GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData()
{
return new TheoryData<BaseItem, IEnumerable<ImageType>>
{
{ new Episode(), new List<ImageType> { ImageType.Primary } },
{ new Movie(), new List<ImageType> { ImageType.Logo, ImageType.Backdrop, ImageType.Primary } },
};
}
[Theory]
[MemberData(nameof(GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData))]
public void GetSupportedImages_SupportedBaseItems_ReturnsPopulated(BaseItem item, IEnumerable<ImageType> expected)
{
var embeddedImageProvider = GetEmbeddedImageProvider(null);
var actual = embeddedImageProvider.GetSupportedImages(item);
Assert.Equal(expected.OrderBy(i => i.ToString()), actual.OrderBy(i => i.ToString()));
}
[Fact]
public async void GetImage_InputWithNoStreams_ReturnsNoImage()
{
var embeddedImageProvider = GetEmbeddedImageProvider(null);
var input = GetMovie(new List<MediaAttachment>(), new List<MediaStream>());
var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_InputWithUnlabeledAttachments_ReturnsNoImage()
{
var embeddedImageProvider = GetEmbeddedImageProvider(null);
// add an attachment without a filename - has a list to look through but finds nothing
var input = GetMovie(
new List<MediaAttachment> { new () },
new List<MediaStream>());
var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_InputWithLabeledAttachments_ReturnsCorrectSelection()
{
// first tests file extension detection, second uses mimetype, third defaults to jpg
MediaAttachment sampleAttachment1 = new () { FileName = "clearlogo.png", Index = 1 };
MediaAttachment sampleAttachment2 = new () { FileName = "backdrop", MimeType = "image/bmp", Index = 2 };
MediaAttachment sampleAttachment3 = new () { FileName = "poster", Index = 3 };
string targetPath1 = "path1.png";
string targetPath2 = "path2.bmp";
string targetPath3 = "path2.jpg";
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 1, ".png", CancellationToken.None))
.Returns(Task.FromResult(targetPath1));
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 2, ".bmp", CancellationToken.None))
.Returns(Task.FromResult(targetPath2));
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 3, ".jpg", CancellationToken.None))
.Returns(Task.FromResult(targetPath3));
var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
var input = GetMovie(
new List<MediaAttachment> { sampleAttachment1, sampleAttachment2, sampleAttachment3 },
new List<MediaStream>());
var actualLogo = await embeddedImageProvider.GetImage(input, ImageType.Logo, CancellationToken.None);
Assert.NotNull(actualLogo);
Assert.True(actualLogo.HasImage);
Assert.Equal(targetPath1, actualLogo.Path);
Assert.Equal(ImageFormat.Png, actualLogo.Format);
var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
Assert.NotNull(actualBackdrop);
Assert.True(actualBackdrop.HasImage);
Assert.Equal(targetPath2, actualBackdrop.Path);
Assert.Equal(ImageFormat.Bmp, actualBackdrop.Format);
var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actualPrimary);
Assert.True(actualPrimary.HasImage);
Assert.Equal(targetPath3, actualPrimary.Path);
Assert.Equal(ImageFormat.Jpg, actualPrimary.Format);
}
[Fact]
public async void GetImage_InputWithUnlabeledEmbeddedImages_BackdropReturnsNoImage()
{
var embeddedImageProvider = GetEmbeddedImageProvider(null);
var input = GetMovie(
new List<MediaAttachment>(),
new List<MediaStream> { new () { Type = MediaStreamType.EmbeddedImage } });
var actual = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_InputWithUnlabeledEmbeddedImages_PrimaryReturnsImage()
{
MediaStream sampleStream = new () { Type = MediaStreamType.EmbeddedImage, Index = 1 };
string targetPath = "path";
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream, 1, ".jpg", CancellationToken.None))
.Returns(Task.FromResult(targetPath));
var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
var input = GetMovie(
new List<MediaAttachment>(),
new List<MediaStream> { sampleStream });
var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.True(actual.HasImage);
Assert.Equal(targetPath, actual.Path);
Assert.Equal(ImageFormat.Jpg, actual.Format);
}
[Fact]
public async void GetImage_InputWithLabeledEmbeddedImages_ReturnsCorrectSelection()
{
// primary is second stream to ensure it's not defaulting, backdrop is first
MediaStream sampleStream1 = new () { Type = MediaStreamType.EmbeddedImage, Index = 1, Comment = "backdrop" };
MediaStream sampleStream2 = new () { Type = MediaStreamType.EmbeddedImage, Index = 2, Comment = "cover" };
string targetPath1 = "path1.jpg";
string targetPath2 = "path2.jpg";
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream1, 1, ".jpg", CancellationToken.None))
.Returns(Task.FromResult(targetPath1));
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream2, 2, ".jpg", CancellationToken.None))
.Returns(Task.FromResult(targetPath2));
var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
var input = GetMovie(
new List<MediaAttachment>(),
new List<MediaStream> { sampleStream1, sampleStream2 });
var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actualPrimary);
Assert.True(actualPrimary.HasImage);
Assert.Equal(targetPath2, actualPrimary.Path);
Assert.Equal(ImageFormat.Jpg, actualPrimary.Format);
var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
Assert.NotNull(actualBackdrop);
Assert.True(actualBackdrop.HasImage);
Assert.Equal(targetPath1, actualBackdrop.Path);
Assert.Equal(ImageFormat.Jpg, actualBackdrop.Format);
}
private static EmbeddedImageProvider GetEmbeddedImageProvider(IMediaEncoder? mediaEncoder)
{
return new EmbeddedImageProvider(mediaEncoder);
}
private static Movie GetMovie(List<MediaAttachment> mediaAttachments, List<MediaStream> mediaStreams)
{
// Mocking IMediaSourceManager GetMediaAttachments and GetMediaStreams instead of mocking Movie works, but
// has concurrency problems between this and VideoImageProviderTests due to BaseItem.MediaSourceManager
// being static
var movie = new Mock<Movie>();
movie.Setup(item => item.GetMediaSources(It.IsAny<bool>()))
.Returns(new List<MediaSourceInfo> { new () { MediaAttachments = mediaAttachments } } );
movie.Setup(item => item.GetMediaStreams())
.Returns(mediaStreams);
return movie.Object;
}
}
}

View File

@@ -1,4 +1,4 @@
#pragma warning disable CA1002 // Do not expose generic lists
#pragma warning disable CA1002 // Do not expose generic lists
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
@@ -11,11 +11,12 @@ namespace Jellyfin.Providers.Tests.MediaInfo
{
public class SubtitleResolverTests
{
public static IEnumerable<object[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData()
public static TheoryData<List<MediaStream>, string, int, string[], MediaStream[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData()
{
var data = new TheoryData<List<MediaStream>, string, int, string[], MediaStream[]>();
var index = 0;
yield return new object[]
{
data.Add(
new List<MediaStream>(),
"/video/My.Video.mkv",
index,
@@ -52,8 +53,9 @@ namespace Jellyfin.Providers.Tests.MediaInfo
CreateMediaStream("/video/My.Video.default.forced.en.srt", "srt", "en", index++, isForced: true, isDefault: true),
CreateMediaStream("/video/My.Video.en.default.forced.srt", "srt", "en", index++, isForced: true, isDefault: true),
CreateMediaStream("/video/My.Video.With.Additional.Garbage.en.srt", "srt", "en", index),
}
};
});
return data;
}
[Theory]

View File

@@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Providers.MediaInfo;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Providers.Tests.MediaInfo
{
public class VideoImageProviderTests
{
[Fact]
public async void GetImage_InputIsPlaceholder_ReturnsNoImage()
{
var videoImageProvider = GetVideoImageProvider(null);
var input = new Movie
{
IsPlaceHolder = true
};
var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_NoDefaultVideoStream_ReturnsNoImage()
{
var videoImageProvider = GetVideoImageProvider(null);
var input = new Movie
{
DefaultVideoStreamIndex = null
};
var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_DefaultSetButNoVideoStream_ReturnsNoImage()
{
var videoImageProvider = GetVideoImageProvider(null);
// set a default index but don't put anything there (invalid input, but provider shouldn't break)
var input = GetMovie(0, null, new List<MediaStream>());
var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_DefaultSetMultipleVideoStreams_ReturnsDefaultStreamImage()
{
MediaStream firstStream = new () { Type = MediaStreamType.Video, Index = 0 };
MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 1 };
string targetPath = "path.jpg";
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), firstStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
.Returns(Task.FromResult("wrong stream called!"));
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), targetStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
.Returns(Task.FromResult(targetPath));
var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
var input = GetMovie(1, targetStream, new List<MediaStream> { firstStream, targetStream } );
var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.True(actual.HasImage);
Assert.Equal(targetPath, actual.Path);
Assert.Equal(ImageFormat.Jpg, actual.Format);
}
[Fact]
public async void GetImage_InvalidDefaultSingleVideoStream_ReturnsFirstVideoStreamImage()
{
MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
string targetPath = "path.jpg";
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), targetStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
.Returns(Task.FromResult(targetPath));
var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
// provide query results for default (empty) and all streams (populated)
var input = GetMovie(5, null, new List<MediaStream> { targetStream });
var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.True(actual.HasImage);
Assert.Equal(targetPath, actual.Path);
Assert.Equal(ImageFormat.Jpg, actual.Format);
}
[Fact]
public async void GetImage_NoTimeSpanSet_CallsEncoderWithDefaultTime()
{
MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
// use a callback to catch the actual value
// provides more information on failure than verifying a specific input was called on the mock
TimeSpan? actualTimeSpan = null;
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
.Callback<string, string, MediaSourceInfo, MediaStream, Video3DFormat?, TimeSpan?, CancellationToken>((_, _, _, _, _, timeSpan, _) => actualTimeSpan = timeSpan)
.Returns(Task.FromResult("path"));
var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
var input = GetMovie(0, targetStream, new List<MediaStream> { targetStream });
// not testing return, just verifying what gets requested for time span
await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.Equal(TimeSpan.FromSeconds(10), actualTimeSpan);
}
[Fact]
public async void GetImage_TimeSpanSet_CallsEncoderWithCalculatedTime()
{
MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
TimeSpan? actualTimeSpan = null;
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
.Callback<string, string, MediaSourceInfo, MediaStream, Video3DFormat?, TimeSpan?, CancellationToken>((_, _, _, _, _, timeSpan, _) => actualTimeSpan = timeSpan)
.Returns(Task.FromResult("path"));
var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
var input = GetMovie(0, targetStream, new List<MediaStream> { targetStream });
input.RunTimeTicks = 5000;
// not testing return, just verifying what gets requested for time span
await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.Equal(TimeSpan.FromTicks(500), actualTimeSpan);
}
private static VideoImageProvider GetVideoImageProvider(IMediaEncoder? mediaEncoder)
{
// strict to ensure this isn't accidentally used where a prepared mock is intended
mediaEncoder ??= new Mock<IMediaEncoder>(MockBehavior.Strict).Object;
return new VideoImageProvider(mediaEncoder, new NullLogger<VideoImageProvider>());
}
private static Movie GetMovie(int defaultVideoStreamIndex, MediaStream? defaultStream, List<MediaStream> mediaStreams)
{
// Mocking IMediaSourceManager GetMediaStreams instead of mocking Movie works, but has concurrency problems
// between this and EmbeddedImageProviderTests due to BaseItem.MediaSourceManager being static
var movie = new Mock<Movie>
{
Object =
{
DefaultVideoStreamIndex = defaultVideoStreamIndex
}
};
movie.Setup(item => item.GetDefaultVideoStream())
.Returns(defaultStream!);
movie.Setup(item => item.GetMediaStreams())
.Returns(mediaStreams);
return movie.Object;
}
}
}

View File

@@ -23,5 +23,18 @@ namespace Jellyfin.Providers.Tests.Tmdb
{
Assert.Equal(expected, TmdbUtils.NormalizeLanguage(input!));
}
[Theory]
[InlineData(null, null, null)]
[InlineData(null, "en-US", null)]
[InlineData("en", null, "en")]
[InlineData("en", "en-US", "en-US")]
[InlineData("fr-CA", "fr-BE", "fr-CA")]
[InlineData("fr-CA", "fr", "fr-CA")]
[InlineData("de", "en-US", "de")]
public static void AdjustImageLanguage_Valid_Success(string imageLanguage, string requestLanguage, string expected)
{
Assert.Equal(expected, TmdbUtils.AdjustImageLanguage(imageLanguage, requestLanguage));
}
}
}

View File

@@ -32,10 +32,11 @@ namespace Jellyfin.Server.Implementations.Tests.Data
_sqliteItemRepository = _fixture.Create<SqliteItemRepository>();
}
public static IEnumerable<object[]> ItemImageInfoFromValueString_Valid_TestData()
public static TheoryData<string, ItemImageInfo> ItemImageInfoFromValueString_Valid_TestData()
{
yield return new object[]
{
var data = new TheoryData<string, ItemImageInfo>();
data.Add(
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN",
new ItemImageInfo
{
@@ -45,41 +46,33 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Width = 1920,
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
}
};
});
yield return new object[]
{
data.Add(
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*0*0",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
}
};
});
yield return new object[]
{
data.Add(
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
}
};
});
yield return new object[]
{
data.Add(
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*600",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
}
};
});
yield return new object[]
{
data.Add(
"%MetadataPath%/library/68/68578562b96c80a7ebd530848801f645/poster.jpg*637264380567586027*Primary*600*336",
new ItemImageInfo
{
@@ -88,8 +81,9 @@ namespace Jellyfin.Server.Implementations.Tests.Data
DateModified = new DateTime(637264380567586027, DateTimeKind.Utc),
Width = 600,
Height = 336
}
};
});
return data;
}
[Theory]
@@ -117,10 +111,10 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Assert.Null(_sqliteItemRepository.ItemImageInfoFromValueString(value));
}
public static IEnumerable<object[]> DeserializeImages_Valid_TestData()
public static TheoryData<string, ItemImageInfo[]> DeserializeImages_Valid_TestData()
{
yield return new object[]
{
var data = new TheoryData<string, ItemImageInfo[]>();
data.Add(
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN",
new ItemImageInfo[]
{
@@ -133,11 +127,9 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
}
}
};
});
yield return new object[]
{
data.Add(
"%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/poster.jpg*637261226720645297*Primary*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/logo.png*637261226720805297*Logo*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/landscape.jpg*637261226721285297*Thumb*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/backdrop.jpg*637261226721685297*Backdrop*0*0",
new ItemImageInfo[]
{
@@ -165,20 +157,19 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Type = ImageType.Backdrop,
DateModified = new DateTime(637261226721685297, DateTimeKind.Utc),
}
}
};
});
return data;
}
public static IEnumerable<object[]> DeserializeImages_ValidAndInvalid_TestData()
public static TheoryData<string, ItemImageInfo[]> DeserializeImages_ValidAndInvalid_TestData()
{
yield return new object[]
{
var data = new TheoryData<string, ItemImageInfo[]>();
data.Add(
string.Empty,
Array.Empty<ItemImageInfo>()
};
Array.Empty<ItemImageInfo>());
yield return new object[]
{
data.Add(
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN|test|1234||ss",
new ItemImageInfo[]
{
@@ -191,14 +182,13 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
}
}
};
});
yield return new object[]
{
data.Add(
"|",
Array.Empty<ItemImageInfo>()
};
Array.Empty<ItemImageInfo>());
return data;
}
[Theory]
@@ -242,30 +232,27 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Assert.Equal(expected, _sqliteItemRepository.SerializeImages(value));
}
public static IEnumerable<object[]> DeserializeProviderIds_Valid_TestData()
public static TheoryData<string, Dictionary<string, string>> DeserializeProviderIds_Valid_TestData()
{
yield return new object[]
{
var data = new TheoryData<string, Dictionary<string, string>>();
data.Add(
"Imdb=tt0119567",
new Dictionary<string, string>()
{
{ "Imdb", "tt0119567" },
}
};
});
yield return new object[]
{
data.Add(
"Imdb=tt0119567|Tmdb=330|TmdbCollection=328",
new Dictionary<string, string>()
{
{ "Imdb", "tt0119567" },
{ "Tmdb", "330" },
{ "TmdbCollection", "328" },
}
};
});
yield return new object[]
{
data.Add(
"MusicBrainzAlbum=9d363e43-f24f-4b39-bc5a-7ef305c677c7|MusicBrainzReleaseGroup=63eba062-847c-3b73-8b0f-6baf27bba6fa|AudioDbArtist=111352|AudioDbAlbum=2116560|MusicBrainzAlbumArtist=20244d07-534f-4eff-b4d4-930878889970",
new Dictionary<string, string>()
{
@@ -274,8 +261,9 @@ namespace Jellyfin.Server.Implementations.Tests.Data
{ "AudioDbArtist", "111352" },
{ "AudioDbAlbum", "2116560" },
{ "MusicBrainzAlbumArtist", "20244d07-534f-4eff-b4d4-930878889970" },
}
};
});
return data;
}
[Theory]

View File

@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
<RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
@@ -21,7 +21,7 @@
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using Emby.Server.Implementations.LiveTv.EmbyTV;
using MediaBrowser.Controller.LiveTv;
using Xunit;
@@ -8,43 +7,36 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
public static class RecordingHelperTests
{
public static IEnumerable<object[]> GetRecordingName_Success_TestData()
public static TheoryData<string, TimerInfo> GetRecordingName_Success_TestData()
{
yield return new object[]
{
var data = new TheoryData<string, TimerInfo>();
data.Add(
"The Incredibles 2020_04_20_21_06_00",
new TimerInfo
{
Name = "The Incredibles",
StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local),
IsMovie = true
}
};
});
yield return new object[]
{
data.Add(
"The Incredibles (2004)",
new TimerInfo
{
Name = "The Incredibles",
IsMovie = true,
ProductionYear = 2004
}
};
yield return new object[]
{
});
data.Add(
"The Big Bang Theory 2020_04_20_21_06_00",
new TimerInfo
{
Name = "The Big Bang Theory",
StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local),
IsProgramSeries = true,
}
};
yield return new object[]
{
});
data.Add(
"The Big Bang Theory S12E10",
new TimerInfo
{
@@ -52,11 +44,8 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
IsProgramSeries = true,
SeasonNumber = 12,
EpisodeNumber = 10
}
};
yield return new object[]
{
});
data.Add(
"The Big Bang Theory S12E10 The VCR Illumination",
new TimerInfo
{
@@ -65,22 +54,17 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
SeasonNumber = 12,
EpisodeNumber = 10,
EpisodeTitle = "The VCR Illumination"
}
};
yield return new object[]
{
});
data.Add(
"The Big Bang Theory 2018-12-06",
new TimerInfo
{
Name = "The Big Bang Theory",
IsProgramSeries = true,
OriginalAirDate = new DateTime(2018, 12, 6)
}
};
});
yield return new object[]
{
data.Add(
"The Big Bang Theory 2018-12-06 - The VCR Illumination",
new TimerInfo
{
@@ -88,11 +72,9 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
IsProgramSeries = true,
OriginalAirDate = new DateTime(2018, 12, 6),
EpisodeTitle = "The VCR Illumination"
}
};
});
yield return new object[]
{
data.Add(
"The Big Bang Theory 2018_12_06_21_06_00 - The VCR Illumination",
new TimerInfo
{
@@ -101,8 +83,9 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
IsProgramSeries = true,
OriginalAirDate = new DateTime(2018, 12, 6),
EpisodeTitle = "The VCR Illumination"
}
};
});
return data;
}
[Theory]

View File

@@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
using Jellyfin.Extensions.Json;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect
{
public class SchedulesDirectDeserializeTests
{
private readonly JsonSerializerOptions _jsonOptions;
public SchedulesDirectDeserializeTests()
{
_jsonOptions = JsonDefaults.Options;
}
/// <summary>
/// /token reponse.
/// </summary>
[Fact]
public void Deserialize_Token_Response_Live_Success()
{
var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/token_live_response.json");
var tokenDto = JsonSerializer.Deserialize<TokenDto>(bytes, _jsonOptions);
Assert.NotNull(tokenDto);
Assert.Equal(0, tokenDto!.Code);
Assert.Equal("OK", tokenDto.Message);
Assert.Equal("AWS-SD-web.1", tokenDto.ServerId);
Assert.Equal(new DateTime(2016, 08, 23, 13, 55, 25, DateTimeKind.Utc), tokenDto.TokenTimestamp);
Assert.Equal("f3fca79989cafe7dead71beefedc812b", tokenDto.Token);
}
/// <summary>
/// /token response.
/// </summary>
[Fact]
public void Deserialize_Token_Response_Offline_Success()
{
var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/token_offline_response.json");
var tokenDto = JsonSerializer.Deserialize<TokenDto>(bytes, _jsonOptions);
Assert.NotNull(tokenDto);
Assert.Equal(3_000, tokenDto!.Code);
Assert.Equal("Server offline for maintenance.", tokenDto.Message);
Assert.Equal("20141201.web.1", tokenDto.ServerId);
Assert.Equal(new DateTime(2015, 04, 23, 00, 03, 32, DateTimeKind.Utc), tokenDto.TokenTimestamp);
Assert.Equal("CAFEDEADBEEFCAFEDEADBEEFCAFEDEADBEEFCAFE", tokenDto.Token);
Assert.Equal("SERVICE_OFFLINE", tokenDto.Response);
}
/// <summary>
/// /schedules request.
/// </summary>
[Fact]
public void Serialize_Schedule_Request_Success()
{
var expectedString = File.ReadAllText("Test Data/SchedulesDirect/schedules_request.json").Trim();
var requestObject = new RequestScheduleForChannelDto[]
{
new RequestScheduleForChannelDto
{
StationId = "20454",
Date = new[]
{
"2015-03-13",
"2015-03-17"
}
},
new RequestScheduleForChannelDto
{
StationId = "10021",
Date = new[]
{
"2015-03-12",
"2015-03-13"
}
}
};
var requestString = JsonSerializer.Serialize(requestObject, _jsonOptions);
Assert.Equal(expectedString, requestString);
}
/// <summary>
/// /schedules response.
/// </summary>
[Fact]
public void Deserialize_Schedule_Response_Success()
{
var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/schedules_response.json");
var days = JsonSerializer.Deserialize<IReadOnlyList<DayDto>>(bytes, _jsonOptions);
Assert.NotNull(days);
Assert.Equal(1, days!.Count);
var dayDto = days[0];
Assert.Equal("20454", dayDto.StationId);
Assert.Equal(2, dayDto.Programs.Count);
Assert.Equal("SH005371070000", dayDto.Programs[0].ProgramId);
Assert.Equal(new DateTime(2015, 03, 03, 00, 00, 00, DateTimeKind.Utc), dayDto.Programs[0].AirDateTime);
Assert.Equal(1_800, dayDto.Programs[0].Duration);
Assert.Equal("Sy8HEMBPcuiAx3FBukUhKQ", dayDto.Programs[0].Md5);
Assert.True(dayDto.Programs[0].New);
Assert.Equal(2, dayDto.Programs[0].AudioProperties.Count);
Assert.Equal("stereo", dayDto.Programs[0].AudioProperties[0]);
Assert.Equal("cc", dayDto.Programs[0].AudioProperties[1]);
Assert.Equal(1, dayDto.Programs[0].VideoProperties.Count);
Assert.Equal("hdtv", dayDto.Programs[0].VideoProperties[0]);
}
/// <summary>
/// /programs response.
/// </summary>
[Fact]
public void Deserialize_Program_Response_Success()
{
var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/programs_response.json");
var programDtos = JsonSerializer.Deserialize<IReadOnlyList<ProgramDetailsDto>>(bytes, _jsonOptions);
Assert.NotNull(programDtos);
Assert.Equal(2, programDtos!.Count);
Assert.Equal("EP000000060003", programDtos[0].ProgramId);
Assert.Equal(1, programDtos[0].Titles.Count);
Assert.Equal("'Allo 'Allo!", programDtos[0].Titles[0].Title120);
Assert.Equal("Series", programDtos[0].EventDetails?.SubType);
Assert.Equal("en", programDtos[0].Descriptions?.Description1000[0].DescriptionLanguage);
Assert.Equal("A disguised British Intelligence officer is sent to help the airmen.", programDtos[0].Descriptions?.Description1000[0].Description);
Assert.Equal(new DateTime(1985, 11, 04), programDtos[0].OriginalAirDate);
Assert.Equal(1, programDtos[0].Genres.Count);
Assert.Equal("Sitcom", programDtos[0].Genres[0]);
Assert.Equal("The Poloceman Cometh", programDtos[0].EpisodeTitle150);
Assert.Equal(2, programDtos[0].Metadata[0].Gracenote?.Season);
Assert.Equal(3, programDtos[0].Metadata[0].Gracenote?.Episode);
Assert.Equal(13, programDtos[0].Cast.Count);
Assert.Equal("383774", programDtos[0].Cast[0].PersonId);
Assert.Equal("392649", programDtos[0].Cast[0].NameId);
Assert.Equal("Gorden Kaye", programDtos[0].Cast[0].Name);
Assert.Equal("Actor", programDtos[0].Cast[0].Role);
Assert.Equal("01", programDtos[0].Cast[0].BillingOrder);
Assert.Equal(3, programDtos[0].Crew.Count);
Assert.Equal("354407", programDtos[0].Crew[0].PersonId);
Assert.Equal("363281", programDtos[0].Crew[0].NameId);
Assert.Equal("David Croft", programDtos[0].Crew[0].Name);
Assert.Equal("Director", programDtos[0].Crew[0].Role);
Assert.Equal("01", programDtos[0].Crew[0].BillingOrder);
}
/// <summary>
/// /metadata/programs response.
/// </summary>
[Fact]
public void Deserialize_Metadata_Programs_Response_Success()
{
var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/metadata_programs_response.json");
var showImagesDtos = JsonSerializer.Deserialize<IReadOnlyList<ShowImagesDto>>(bytes, _jsonOptions);
Assert.NotNull(showImagesDtos);
Assert.Equal(1, showImagesDtos!.Count);
Assert.Equal("SH00712240", showImagesDtos[0].ProgramId);
Assert.Equal(4, showImagesDtos[0].Data.Count);
Assert.Equal("135", showImagesDtos[0].Data[0].Width);
Assert.Equal("180", showImagesDtos[0].Data[0].Height);
Assert.Equal("assets/p282288_b_v2_aa.jpg", showImagesDtos[0].Data[0].Uri);
Assert.Equal("Sm", showImagesDtos[0].Data[0].Size);
Assert.Equal("3x4", showImagesDtos[0].Data[0].Aspect);
Assert.Equal("Banner-L3", showImagesDtos[0].Data[0].Category);
Assert.Equal("yes", showImagesDtos[0].Data[0].Text);
Assert.Equal("true", showImagesDtos[0].Data[0].Primary);
Assert.Equal("Series", showImagesDtos[0].Data[0].Tier);
}
/// <summary>
/// /headends response.
/// </summary>
[Fact]
public void Deserialize_Headends_Response_Success()
{
var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/headends_response.json");
var headendsDtos = JsonSerializer.Deserialize<IReadOnlyList<HeadendsDto>>(bytes, _jsonOptions);
Assert.NotNull(headendsDtos);
Assert.Equal(8, headendsDtos!.Count);
Assert.Equal("CA00053", headendsDtos[0].Headend);
Assert.Equal("Cable", headendsDtos[0].Transport);
Assert.Equal("Beverly Hills", headendsDtos[0].Location);
Assert.Equal(2, headendsDtos[0].Lineups.Count);
Assert.Equal("Time Warner Cable - Cable", headendsDtos[0].Lineups[0].Name);
Assert.Equal("USA-CA00053-DEFAULT", headendsDtos[0].Lineups[0].Lineup);
Assert.Equal("/20141201/lineups/USA-CA00053-DEFAULT", headendsDtos[0].Lineups[0].Uri);
}
/// <summary>
/// /lineups response.
/// </summary>
[Fact]
public void Deserialize_Lineups_Response_Success()
{
var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/lineups_response.json");
var lineupsDto = JsonSerializer.Deserialize<LineupsDto>(bytes, _jsonOptions);
Assert.NotNull(lineupsDto);
Assert.Equal(0, lineupsDto!.Code);
Assert.Equal("20141201.web.1", lineupsDto.ServerId);
Assert.Equal(new DateTime(2015, 04, 17, 14, 22, 17, DateTimeKind.Utc), lineupsDto.LineupTimestamp);
Assert.Equal(5, lineupsDto.Lineups.Count);
Assert.Equal("GBR-0001317-DEFAULT", lineupsDto.Lineups[0].Lineup);
Assert.Equal("Freeview - Carlton - LWT (Southeast)", lineupsDto.Lineups[0].Name);
Assert.Equal("DVB-T", lineupsDto.Lineups[0].Transport);
Assert.Equal("London", lineupsDto.Lineups[0].Location);
Assert.Equal("/20141201/lineups/GBR-0001317-DEFAULT", lineupsDto.Lineups[0].Uri);
Assert.Equal("DELETED LINEUP", lineupsDto.Lineups[4].Name);
Assert.True(lineupsDto.Lineups[4].IsDeleted);
}
/// <summary>
/// /lineup/:id response.
/// </summary>
[Fact]
public void Deserialize_Lineup_Response_Success()
{
var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/lineup_response.json");
var channelDto = JsonSerializer.Deserialize<ChannelDto>(bytes, _jsonOptions);
Assert.NotNull(channelDto);
Assert.Equal(2, channelDto!.Map.Count);
Assert.Equal("24326", channelDto.Map[0].StationId);
Assert.Equal("001", channelDto.Map[0].Channel);
Assert.Equal("BBC ONE South", channelDto.Map[0].ProvderCallsign);
Assert.Equal("1", channelDto.Map[0].LogicalChannelNumber);
Assert.Equal("providerCallsign", channelDto.Map[0].MatchType);
}
}
}

View File

@@ -40,7 +40,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var cultures = localizationManager.GetCultures().ToList();
Assert.Equal(189, cultures.Count);
Assert.Equal(190, cultures.Count);
var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
Assert.NotNull(germany);

View File

@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.QuickConnect;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
@@ -51,6 +52,21 @@ namespace Jellyfin.Server.Implementations.Tests.QuickConnect
public void IsEnabled_QuickConnectUnavailable_False()
=> Assert.False(_quickConnectManager.IsEnabled);
[Theory]
[InlineData("", "DeviceId", "Client", "1.0.0")]
[InlineData("Device", "", "Client", "1.0.0")]
[InlineData("Device", "DeviceId", "", "1.0.0")]
[InlineData("Device", "DeviceId", "Client", "")]
public void TryConnect_InvalidAuthorizationInfo_ThrowsArgumentException(string device, string deviceId, string client, string version)
=> Assert.Throws<ArgumentException>(() => _quickConnectManager.TryConnect(
new AuthorizationInfo
{
Device = device,
DeviceId = deviceId,
Client = client,
Version = version
}));
[Fact]
public void TryConnect_QuickConnectUnavailable_ThrowsAuthenticationException()
=> Assert.Throws<AuthenticationException>(() => _quickConnectManager.TryConnect(_quickConnectAuthInfo));
@@ -63,6 +79,10 @@ namespace Jellyfin.Server.Implementations.Tests.QuickConnect
public void AuthorizeRequest_QuickConnectUnavailable_ThrowsAuthenticationException()
=> Assert.ThrowsAsync<AuthenticationException>(() => _quickConnectManager.AuthorizeRequest(Guid.Empty, string.Empty));
[Fact]
public void GetAuthorizedRequest_QuickConnectUnavailable_ThrowsAuthenticationException()
=> Assert.Throws<AuthenticationException>(() => _quickConnectManager.GetAuthorizedRequest(string.Empty));
[Fact]
public void IsEnabled_QuickConnectAvailable_True()
{
@@ -79,6 +99,20 @@ namespace Jellyfin.Server.Implementations.Tests.QuickConnect
Assert.Equal(res1, res2);
}
[Fact]
public void CheckRequestStatus_UnknownSecret_ThrowsResourceNotFoundException()
{
_config.QuickConnectAvailable = true;
Assert.Throws<ResourceNotFoundException>(() => _quickConnectManager.CheckRequestStatus("Unknown secret"));
}
[Fact]
public void GetAuthorizedRequest_UnknownSecret_ThrowsResourceNotFoundException()
{
_config.QuickConnectAvailable = true;
Assert.Throws<ResourceNotFoundException>(() => _quickConnectManager.GetAuthorizedRequest("Unknown secret"));
}
[Fact]
public async Task AuthorizeRequest_QuickConnectAvailable_Success()
{

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Emby.Server.Implementations.Sorting;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -13,7 +11,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting
{
[Theory]
[ClassData(typeof(EpisodeBadData))]
public void Compare_GivenNull_ThrowsArgumentNullException(BaseItem x, BaseItem y)
public void Compare_GivenNull_ThrowsArgumentNullException(BaseItem? x, BaseItem? y)
{
var cmp = new AiredEpisodeOrderComparer();
Assert.Throws<ArgumentNullException>(() => cmp.Compare(x, y));
@@ -29,152 +27,138 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting
Assert.Equal(-expected, cmp.Compare(y, x));
}
private class EpisodeBadData : IEnumerable<object?[]>
private class EpisodeBadData : TheoryData<BaseItem?, BaseItem?>
{
public IEnumerator<object?[]> GetEnumerator()
public EpisodeBadData()
{
yield return new object?[] { null, new Episode() };
yield return new object?[] { new Episode(), null };
Add(null, new Episode());
Add(new Episode(), null);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
private class EpisodeTestData : IEnumerable<object?[]>
private class EpisodeTestData : TheoryData<BaseItem, BaseItem, int>
{
public IEnumerator<object?[]> GetEnumerator()
public EpisodeTestData()
{
yield return new object?[]
{
Add(
new Movie(),
new Movie(),
0
};
yield return new object?[]
{
0);
Add(
new Movie(),
new Episode(),
1
};
1);
// Good cases
yield return new object?[]
{
Add(
new Episode(),
new Episode(),
0
};
yield return new object?[]
{
0);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
0
};
yield return new object?[]
{
0);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
1
};
yield return new object?[]
{
1);
Add(
new Episode { ParentIndexNumber = 2, IndexNumber = 1 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
1
};
1);
// Good Specials
yield return new object?[]
{
Add(
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
0
};
yield return new object?[]
{
0);
Add(
new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
1
};
1);
// Specials to Episodes
yield return new object?[]
{
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
1
};
yield return new object?[]
{
1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
1
};
yield return new object?[]
{
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
1
};
1);
yield return new object?[]
{
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
1
};
yield return new object?[]
{
1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
1
};
1);
yield return new object?[]
{
Add(
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
1
};
yield return new object?[]
{
1);
Add(
new Episode { ParentIndexNumber = 3, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1 },
1
};
1);
yield return new object?[]
{
Add(
new Episode { ParentIndexNumber = 3, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
1
};
1);
yield return new object?[]
{
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1 },
1
};
yield return new object?[]
{
1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
1
};
yield return new object?[]
{
1);
Add(
new Episode { ParentIndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
0
};
yield return new object?[]
{
0);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 3 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
1
};
}
1);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// Premiere Date
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
0);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 11, 0, 0, 0) },
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
-1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 11, 0, 0, 0) },
1);
}
}
}
}

View File

@@ -0,0 +1 @@
[{"headend":"CA00053","transport":"Cable","location":"Beverly Hills","lineups":[{"name":"Time Warner Cable - Cable","lineup":"USA-CA00053-DEFAULT","uri":"/20141201/lineups/USA-CA00053-DEFAULT"},{"name":"Time Warner Cable - Digital","lineup":"USA-CA00053-X","uri":"/20141201/lineups/USA-CA00053-X"}]},{"headend":"CA61222","transport":"Cable","location":"Beverly Hills","lineups":[{"name":"Mulholland Estates - Cable","lineup":"USA-CA61222-DEFAULT","uri":"/20141201/lineups/USA-CA61222-DEFAULT"}]},{"headend":"CA66511","transport":"Cable","location":"Los Angeles","lineups":[{"name":"AT&T U-verse TV - Digital","lineup":"USA-CA66511-X","uri":"/20141201/lineups/USA-CA66511-X"}]},{"headend":"CA67309","transport":"Cable","location":"Westchester","lineups":[{"name":"Time Warner Cable Sherman Oaks - Cable","lineup":"USA-CA67309-DEFAULT","uri":"/20141201/lineups/USA-CA67309-DEFAULT"},{"name":"Time Warner Cable Sherman Oaks - Digital","lineup":"USA-CA67309-X","uri":"/20141201/lineups/USA-CA67309-X"}]},{"headend":"CA67310","transport":"Cable","location":"Eagle Rock","lineups":[{"name":"Time Warner Cable City of Los Angeles - Cable","lineup":"USA-CA67310-DEFAULT","uri":"/20141201/lineups/USA-CA67310-DEFAULT"},{"name":"Time Warner Cable City of Los Angeles - Digital","lineup":"USA-CA67310-X","uri":"/20141201/lineups/USA-CA67310-X"}]},{"headend":"DISH803","transport":"Satellite","location":"Los Angeles","lineups":[{"name":"DISH Los Angeles - Satellite","lineup":"USA-DISH803-DEFAULT","uri":"/20141201/lineups/USA-DISH803-DEFAULT"}]},{"headend":"DITV803","transport":"Satellite","location":"Los Angeles","lineups":[{"name":"DIRECTV Los Angeles - Satellite","lineup":"USA-DITV803-DEFAULT","uri":"/20141201/lineups/USA-DITV803-DEFAULT"}]},{"headend":"90210","transport":"Antenna","location":"90210","lineups":[{"name":"Antenna","lineup":"USA-OTA-90210","uri":"/20141201/lineups/USA-OTA-90210"}]}]

View File

@@ -0,0 +1 @@
{"map":[{"stationID":"24326","channel":"001","providerCallsign":"BBC ONE South","logicalChannelNumber":"1","matchType":"providerCallsign"},{"stationID":"17154","channel":"002","providerCallsign":"BBC TWO","logicalChannelNumber":"2","matchType":"providerCallsign"}]}

View File

@@ -0,0 +1 @@
{"code":0,"serverID":"20141201.web.1","datetime":"2015-04-17T14:22:17Z","lineups":[{"lineup":"GBR-0001317-DEFAULT","name":"Freeview - Carlton - LWT (Southeast)","transport":"DVB-T","location":"London","uri":"/20141201/lineups/GBR-0001317-DEFAULT"},{"lineup":"USA-IL57303-X","name":"Comcast Waukegan/Lake Forest Area - Digital","transport":"Cable","location":"Lake Forest","uri":"/20141201/lineups/USA-IL57303-X"},{"lineup":"USA-NY67791-X","name":"Verizon Fios Queens - Digital","transport":"Cable","location":"Fresh Meadows","uri":"/20141201/lineups/USA-NY67791-X"},{"lineup":"USA-OTA-60030","name":"Local Over the Air Broadcast","transport":"Antenna","location":"60030","uri":"/20141201/lineups/USA-OTA-60030"},{"lineup":"USA-WI61859-DEFAULT","name":"DELETED LINEUP","isDeleted":true}]}

View File

@@ -0,0 +1 @@
[{"programID":"SH00712240","data":[{"width":"135","height":"180","uri":"assets/p282288_b_v2_aa.jpg","size":"Sm","aspect":"3x4","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"},{"width":"720","height":"540","uri":"assets/p282288_b_h6_aa.jpg","size":"Lg","aspect":"4x3","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"},{"width":"960","height":"1440","uri":"assets/p282288_b_v8_aa.jpg","size":"Ms","aspect":"2x3","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"},{"width":"180","height":"135","uri":"assets/p282288_b_h5_aa.jpg","size":"Sm","aspect":"4x3","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"}]}]

View File

@@ -0,0 +1 @@
[{"programID":"EP000000060003","titles":[{"title120":"'Allo 'Allo!"}],"eventDetails":{"subType":"Series"},"descriptions":{"description1000":[{"descriptionLanguage":"en","description":"A disguised British Intelligence officer is sent to help the airmen."}]},"originalAirDate":"1985-11-04","genres":["Sitcom"],"episodeTitle150":"The Poloceman Cometh","metadata":[{"Gracenote":{"season":2,"episode":3}}],"cast":[{"personId":"383774","nameId":"392649","name":"Gorden Kaye","role":"Actor","billingOrder":"01"},{"personId":"246840","nameId":"250387","name":"Carmen Silvera","role":"Actor","billingOrder":"02"},{"personId":"376955","nameId":"385830","name":"Rose Hill","role":"Actor","billingOrder":"03"},{"personId":"259773","nameId":"263340","name":"Vicki Michelle","role":"Actor","billingOrder":"04"},{"personId":"353113","nameId":"361987","name":"Kirsten Cooke","role":"Actor","billingOrder":"05"},{"personId":"77787","nameId":"77787","name":"Richard Marner","role":"Actor","billingOrder":"06"},{"personId":"230921","nameId":"234193","name":"Guy Siner","role":"Actor","billingOrder":"07"},{"personId":"374934","nameId":"383809","name":"Kim Hartman","role":"Actor","billingOrder":"08"},{"personId":"369151","nameId":"378026","name":"Richard Gibson","role":"Actor","billingOrder":"09"},{"personId":"343690","nameId":"352564","name":"Arthur Bostrom","role":"Actor","billingOrder":"10"},{"personId":"352557","nameId":"361431","name":"John D. Collins","role":"Actor","billingOrder":"11"},{"personId":"605275","nameId":"627734","name":"Nicholas Frankau","role":"Actor","billingOrder":"12"},{"personId":"373394","nameId":"382269","name":"Jack Haig","role":"Actor","billingOrder":"13"}],"crew":[{"personId":"354407","nameId":"363281","name":"David Croft","role":"Director","billingOrder":"01"},{"personId":"354407","nameId":"363281","name":"David Croft","role":"Writer","billingOrder":"02"},{"personId":"105145","nameId":"105145","name":"Jeremy Lloyd","role":"Writer","billingOrder":"03"}],"showType":"Series","hasImageArtwork":true,"md5":"Jo5NKxoo44xRvBCAq8QT2A"},{"programID":"EP000000510142","titles":[{"title120":"A Different World"}],"eventDetails":{"subType":"Series"},"descriptions":{"description1000":[{"descriptionLanguage":"en","description":"Whitley and Dwayne tell new students about their honeymoon in Los Angeles."}]},"originalAirDate":"1992-09-24","genres":["Sitcom"],"episodeTitle150":"Honeymoon in L.A.","metadata":[{"Gracenote":{"season":6,"episode":1}}],"cast":[{"personId":"700","nameId":"700","name":"Jasmine Guy","role":"Actor","billingOrder":"01"},{"personId":"729","nameId":"729","name":"Kadeem Hardison","role":"Actor","billingOrder":"02"},{"personId":"120","nameId":"120","name":"Darryl M. Bell","role":"Actor","billingOrder":"03"},{"personId":"1729","nameId":"1729","name":"Cree Summer","role":"Actor","billingOrder":"04"},{"personId":"217","nameId":"217","name":"Charnele Brown","role":"Actor","billingOrder":"05"},{"personId":"1811","nameId":"1811","name":"Glynn Turman","role":"Actor","billingOrder":"06"},{"personId":"1232","nameId":"1232","name":"Lou Myers","role":"Actor","billingOrder":"07"},{"personId":"1363","nameId":"1363","name":"Jada Pinkett","role":"Guest Star","billingOrder":"08"},{"personId":"222967","nameId":"225536","name":"Ajai Sanders","role":"Guest Star","billingOrder":"09"},{"personId":"181744","nameId":"183292","name":"Karen Malina White","role":"Guest Star","billingOrder":"10"},{"personId":"305017","nameId":"318897","name":"Patrick Y. Malone","role":"Guest Star","billingOrder":"11"},{"personId":"9841","nameId":"9841","name":"Bumper Robinson","role":"Guest Star","billingOrder":"12"},{"personId":"426422","nameId":"435297","name":"Sister Souljah","role":"Guest Star","billingOrder":"13"},{"personId":"25","nameId":"25","name":"Debbie Allen","role":"Guest Star","billingOrder":"14"},{"personId":"668","nameId":"668","name":"Gilbert Gottfried","role":"Guest Star","billingOrder":"15"}],"showType":"Series","hasImageArtwork":true,"md5":"P5kz0QmCeYxIA+yL0H4DWw"}]

View File

@@ -0,0 +1 @@
[{"stationID":"20454","date":["2015-03-13","2015-03-17"]},{"stationID":"10021","date":["2015-03-12","2015-03-13"]}]

View File

@@ -0,0 +1 @@
[{"stationID":"20454","programs":[{"programID":"SH005371070000","airDateTime":"2015-03-03T00:00:00Z","duration":1800,"md5":"Sy8HEMBPcuiAx3FBukUhKQ","new":true,"audioProperties":["stereo","cc"],"videoProperties":["hdtv"]},{"programID":"EP000014577244","airDateTime":"2015-03-03T00:30:00Z","duration":1800,"md5":"25DNXVXO192JI7Y9vSW9lQ","new":true,"audioProperties":["stereo","cc"],"videoProperties":["hdtv"]}]}]

View File

@@ -0,0 +1 @@
{"code":0,"message":"OK","serverID":"AWS-SD-web.1","datetime":"2016-08-23T13:55:25Z","token":"f3fca79989cafe7dead71beefedc812b"}

View File

@@ -0,0 +1 @@
{"response":"SERVICE_OFFLINE","code":3000,"serverID":"20141201.web.1","message":"Server offline for maintenance.","datetime":"2015-04-23T00:03:32Z","token":"CAFEDEADBEEFCAFEDEADBEEFCAFEDEADBEEFCAFE"}

View File

@@ -6,7 +6,9 @@ using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.Archiving;
using Emby.Server.Implementations.Updates;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Updates;
using Moq;
using Moq.Protected;
@@ -40,7 +42,9 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
_fixture.Customize(new AutoMoqCustomization
{
ConfigureMembers = true
}).Inject(http);
});
_fixture.Inject(http);
_fixture.Inject<IZipClient>(new ZipClient());
_installationManager = _fixture.Create<InstallationManager>();
}
@@ -78,5 +82,32 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
packages = _installationManager.FilterPackages(packages, id: new Guid("a4df60c5-6ab4-412a-8f79-2cab93fb2bc5")).ToArray();
Assert.Single(packages);
}
[Fact]
public async Task InstallPackage_InvalidChecksum_ThrowsInvalidDataException()
{
var packageInfo = new InstallationInfo()
{
Name = "Test",
SourceUrl = "https://repo.jellyfin.org/releases/plugin/empty/empty.zip",
Checksum = "InvalidChecksum"
};
await Assert.ThrowsAsync<InvalidDataException>(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false);
}
[Fact]
public async Task InstallPackage_Valid_Success()
{
var packageInfo = new InstallationInfo()
{
Name = "Test",
SourceUrl = "https://repo.jellyfin.org/releases/plugin/empty/empty.zip",
Checksum = "11b5b2f1a9ebc4f66d6ef19018543361"
};
var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false);
Assert.Null(ex);
}
}
}

View File

@@ -0,0 +1,141 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
using MediaBrowser.Model.Dlna;
using Xunit;
using Xunit.Priority;
namespace Jellyfin.Server.Integration.Tests.Controllers
{
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class DlnaControllerTests : IClassFixture<JellyfinApplicationFactory>
{
private const string NonExistentProfile = "1322f35b8f2c434dad3cc07c9b97dbd1";
private readonly JellyfinApplicationFactory _factory;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private static string? _accessToken;
private static string? _newDeviceProfileId;
public DlnaControllerTests(JellyfinApplicationFactory factory)
{
_factory = factory;
}
[Fact]
[Priority(0)]
public async Task GetProfile_DoesNotExist_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
using var getResponse = await client.GetAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
}
[Fact]
[Priority(0)]
public async Task DeleteProfile_DoesNotExist_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
using var getResponse = await client.DeleteAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
}
[Fact]
[Priority(0)]
public async Task UpdateProfile_DoesNotExist_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
var deviceProfile = new DeviceProfile()
{
Name = "ThisProfileDoesNotExist"
};
using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(deviceProfile, _jsonOptions));
content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
using var getResponse = await client.PostAsync("/Dlna/Profiles/" + NonExistentProfile, content).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
}
[Fact]
[Priority(1)]
public async Task CreateProfile_Valid_NoContent()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
var deviceProfile = new DeviceProfile()
{
Name = "ThisProfileIsNew"
};
using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(deviceProfile, _jsonOptions));
content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
using var getResponse = await client.PostAsync("/Dlna/Profiles", content).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
}
[Fact]
[Priority(2)]
public async Task GetProfileInfos_Valid_ContainsThisProfileIsNew()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
using var response = await client.GetAsync("/Dlna/ProfileInfos").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>(
await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
_jsonOptions).ConfigureAwait(false);
var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal));
Assert.NotNull(newProfile);
_newDeviceProfileId = newProfile!.Id;
}
[Fact]
[Priority(3)]
public async Task UpdateProfile_Valid_NoContent()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
var updatedProfile = new DeviceProfile()
{
Name = "ThisProfileIsUpdated",
Id = _newDeviceProfileId
};
using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(updatedProfile, _jsonOptions));
content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
using var getResponse = await client.PostAsync("/Dlna/Profiles", content).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
}
[Fact]
[Priority(4)]
public async Task DeleteProfile_Valid_NoContent()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
using var getResponse = await client.DeleteAsync("/Dlna/Profiles/" + _newDeviceProfileId).ConfigureAwait(false);
Console.WriteLine(await getResponse.Content.ReadAsStringAsync().ConfigureAwait(false));
Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
}
}
}

View File

@@ -21,6 +21,7 @@ namespace Jellyfin.Server.Integration.Tests
[InlineData("a=1", "a=1")] // won't be processed as it has a value
[InlineData("a%3D1%26b%3D2%26c%3D3", "a=1&b=2&c=3")] // will be processed.
[InlineData("a=b&a=c", "a=b")]
[InlineData("a%3D1", "a=1")]
[InlineData("a%3Db%26a%3Dc", "a=b")]
public async Task Ensure_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
{

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -9,14 +9,14 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.9" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-rc.2*" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0-rc.2*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
<PackageReference Include="Moq" Version="4.16.0" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,7 +3,6 @@ using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using Emby.Server.Implementations;
using Emby.Server.Implementations.IO;
using MediaBrowser.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
@@ -67,7 +66,7 @@ namespace Jellyfin.Server.Integration.Tests
var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths);
ILoggerFactory loggerFactory = new SerilogLoggerFactory();
var serviceCollection = new ServiceCollection();
_disposableComponents.Add(loggerFactory);
// Create the app host and initialize it
@@ -75,11 +74,10 @@ namespace Jellyfin.Server.Integration.Tests
appPaths,
loggerFactory,
commandLineOpts,
new ConfigurationBuilder().Build(),
new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
serviceCollection);
new ConfigurationBuilder().Build());
_disposableComponents.Add(appHost);
appHost.Init();
var serviceCollection = new ServiceCollection();
appHost.Init(serviceCollection);
// Configure the web host builder
Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths);

View File

@@ -2,9 +2,7 @@ using System.Collections.Generic;
using System.Reflection;
using Emby.Server.Implementations;
using MediaBrowser.Controller;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Integration.Tests
@@ -21,22 +19,16 @@ namespace Jellyfin.Server.Integration.Tests
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="startup">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
public TestAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startup,
IFileSystem fileSystem,
IServiceCollection collection)
IConfiguration startup)
: base(
applicationPaths,
loggerFactory,
options,
startup,
fileSystem,
collection)
startup)
{
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -10,13 +10,13 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.9" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-rc.2*" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0-rc.2*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
<PackageReference Include="Moq" Version="4.16.0" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
<!-- Code Analyzers -->

View File

@@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using Jellyfin.Server.Extensions;

View File

@@ -12,9 +12,6 @@ namespace Jellyfin.Server.Tests
{
[Theory]
[InlineData("e0a72cb2a2c7", "e0a72cb2a2c7")] // isn't encoded
[InlineData("random+test", "random test")] // encoded
[InlineData("random%20test", "random test")] // encoded
[InlineData("++", " ")] // encoded
public static void EmptyValueTest(string query, string key)
{
var dict = new Dictionary<string, StringValues>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -13,7 +13,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />

View File

@@ -11,7 +11,6 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.System;
using MediaBrowser.Providers.Plugins.Tmdb.Movies;
using MediaBrowser.XbmcMetadata.Parsers;
using Microsoft.Extensions.Logging.Abstractions;