mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-03-19 08:36:37 +00:00
Move non-jellyfin extensions to separate project
This commit is contained in:
144
src/Jellyfin.Extensions/AlphanumericComparator.cs
Normal file
144
src/Jellyfin.Extensions/AlphanumericComparator.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Alphanumeric <see cref="IComparer{T}" />.
|
||||
/// </summary>
|
||||
public class AlphanumericComparator : IComparer<string?>
|
||||
{
|
||||
/// <summary>
|
||||
/// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
|
||||
/// </summary>
|
||||
/// <param name="s1">The first object to compare.</param>
|
||||
/// <param name="s2">The second object to compare.</param>
|
||||
/// <returns>A signed integer that indicates the relative values of <c>x</c> and <c>y</c>.</returns>
|
||||
public static int CompareValues(string? s1, string? s2)
|
||||
{
|
||||
if (s1 == null && s2 == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if (s1 == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (s2 == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
int len1 = s1.Length;
|
||||
int len2 = s2.Length;
|
||||
|
||||
// Early return for empty strings
|
||||
if (len1 == 0 && len2 == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if (len1 == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (len2 == 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
int pos1 = 0;
|
||||
int pos2 = 0;
|
||||
|
||||
do
|
||||
{
|
||||
int start1 = pos1;
|
||||
int start2 = pos2;
|
||||
|
||||
bool isNum1 = char.IsDigit(s1[pos1++]);
|
||||
bool isNum2 = char.IsDigit(s2[pos2++]);
|
||||
|
||||
while (pos1 < len1 && char.IsDigit(s1[pos1]) == isNum1)
|
||||
{
|
||||
pos1++;
|
||||
}
|
||||
|
||||
while (pos2 < len2 && char.IsDigit(s2[pos2]) == isNum2)
|
||||
{
|
||||
pos2++;
|
||||
}
|
||||
|
||||
var span1 = s1.AsSpan(start1, pos1 - start1);
|
||||
var span2 = s2.AsSpan(start2, pos2 - start2);
|
||||
|
||||
if (isNum1 && isNum2)
|
||||
{
|
||||
// Trim leading zeros so we can compare the length
|
||||
// of the strings to find the largest number
|
||||
span1 = span1.TrimStart('0');
|
||||
span2 = span2.TrimStart('0');
|
||||
var span1Len = span1.Length;
|
||||
var span2Len = span2.Length;
|
||||
if (span1Len < span2Len)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (span1Len > span2Len)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else if (span1Len >= 20) // Number is probably too big for a ulong
|
||||
{
|
||||
// Trim all the first digits that are the same
|
||||
int i = 0;
|
||||
while (i < span1Len && span1[i] == span2[i])
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
// If there are no more digits it's the same number
|
||||
if (i == span1Len)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only need to compare the most significant digit
|
||||
span1 = span1.Slice(i, 1);
|
||||
span2 = span2.Slice(i, 1);
|
||||
}
|
||||
|
||||
if (!ulong.TryParse(span1, out var num1)
|
||||
|| !ulong.TryParse(span2, out var num2))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if (num1 < num2)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (num1 > num2)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int result = span1.CompareTo(span2, StringComparison.InvariantCulture);
|
||||
if (result != 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
#pragma warning disable SA1500 // TODO remove with StyleCop.Analyzers v1.2.0 https://github.com/DotNetAnalyzers/StyleCopAnalyzers/pull/3196
|
||||
} while (pos1 < len1 && pos2 < len2);
|
||||
#pragma warning restore SA1500
|
||||
|
||||
return len1 - len2;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Compare(string? x, string? y)
|
||||
{
|
||||
return CompareValues(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/Jellyfin.Extensions/CopyToExtensions.cs
Normal file
26
src/Jellyfin.Extensions/CopyToExtensions.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides <c>CopyTo</c> extensions methods for <see cref="IReadOnlyList{T}" />.
|
||||
/// </summary>
|
||||
public static class CopyToExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Copies all the elements of the current collection to the specified list
|
||||
/// starting at the specified destination array index. The index is specified as a 32-bit integer.
|
||||
/// </summary>
|
||||
/// <param name="source">The current collection that is the source of the elements.</param>
|
||||
/// <param name="destination">The list that is the destination of the elements copied from the current collection.</param>
|
||||
/// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param>
|
||||
/// <typeparam name="T">The type of the array.</typeparam>
|
||||
public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0)
|
||||
{
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
destination[index + i] = source[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/Jellyfin.Extensions/EnumerableExtensions.cs
Normal file
51
src/Jellyfin.Extensions/EnumerableExtensions.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Static extensions for the <see cref="IEnumerable{T}"/> interface.
|
||||
/// </summary>
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the value is contained in the source collection.
|
||||
/// </summary>
|
||||
/// <param name="source">An instance of the <see cref="IEnumerable{String}"/> interface.</param>
|
||||
/// <param name="value">The value to look for in the collection.</param>
|
||||
/// <param name="stringComparison">The string comparison.</param>
|
||||
/// <returns>A value indicating whether the value is contained in the collection.</returns>
|
||||
/// <exception cref="ArgumentNullException">The source is null.</exception>
|
||||
public static bool Contains(this IEnumerable<string> source, ReadOnlySpan<char> value, StringComparison stringComparison)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
if (source is IList<string> list)
|
||||
{
|
||||
int len = list.Count;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
if (value.Equals(list[i], stringComparison))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (string element in source)
|
||||
{
|
||||
if (value.Equals(element, stringComparison))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
Normal file
30
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
Normal file
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<CodeAnalysisRuleSet>../../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="../../SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a number to a boolean.
|
||||
/// This is needed for HDHomerun.
|
||||
/// </summary>
|
||||
public class JsonBoolNumberConverter : JsonConverter<bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Number)
|
||||
{
|
||||
return Convert.ToBoolean(reader.GetInt32());
|
||||
}
|
||||
|
||||
return reader.GetBoolean();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteBooleanValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert comma delimited string to array of type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type to convert to.</typeparam>
|
||||
public sealed class JsonCommaDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonCommaDelimitedArrayConverter{T}"/> class.
|
||||
/// </summary>
|
||||
public JsonCommaDelimitedArrayConverter() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override char Delimiter => ',';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Json comma delimited array converter factory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
|
||||
/// </remarks>
|
||||
public class JsonCommaDelimitedArrayConverterFactory : JsonConverterFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
|
||||
return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy DateTime converter.
|
||||
/// Milliseconds aren't output if zero by default.
|
||||
/// </summary>
|
||||
public class JsonDateTimeConverter : JsonConverter<DateTime>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.GetDateTime();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value.Millisecond == 0)
|
||||
{
|
||||
// Remaining ticks value will be 0, manually format.
|
||||
writer.WriteStringValue(value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffZ", CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert delimited string to array of type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type to convert to.</typeparam>
|
||||
public abstract class JsonDelimitedArrayConverter<T> : JsonConverter<T[]?>
|
||||
{
|
||||
private readonly TypeConverter _typeConverter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonDelimitedArrayConverter{T}"/> class.
|
||||
/// </summary>
|
||||
protected JsonDelimitedArrayConverter()
|
||||
{
|
||||
_typeConverter = TypeDescriptor.GetConverter(typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the array delimiter.
|
||||
/// </summary>
|
||||
protected virtual char Delimiter { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
// GetString can't return null here because we already handled it above
|
||||
var stringEntries = reader.GetString()?.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (stringEntries == null || stringEntries.Length == 0)
|
||||
{
|
||||
return Array.Empty<T>();
|
||||
}
|
||||
|
||||
var parsedValues = new object[stringEntries.Length];
|
||||
var convertedCount = 0;
|
||||
for (var i = 0; i < stringEntries.Length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
|
||||
convertedCount++;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// TODO log when upgraded to .Net6
|
||||
// https://github.com/dotnet/runtime/issues/42975
|
||||
// _logger.LogDebug(e, "Error converting value.");
|
||||
}
|
||||
}
|
||||
|
||||
var typedValues = new T[convertedCount];
|
||||
var typedValueIndex = 0;
|
||||
for (var i = 0; i < stringEntries.Length; i++)
|
||||
{
|
||||
if (parsedValues[i] != null)
|
||||
{
|
||||
typedValues.SetValue(parsedValues[i], typedValueIndex);
|
||||
typedValueIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return typedValues;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<T[]>(ref reader, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs
Normal file
26
src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a GUID object or value to/from JSON.
|
||||
/// </summary>
|
||||
public class JsonGuidConverter : JsonConverter<Guid>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var guidStr = reader.GetString();
|
||||
return guidStr == null ? Guid.Empty : new Guid(guidStr);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an object to a lowercase string.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The object type.</typeparam>
|
||||
public class JsonLowerCaseConverter<T> : JsonConverter<T>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(ref reader, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value?.ToString()?.ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a GUID object or value to/from JSON.
|
||||
/// </summary>
|
||||
public class JsonNullableGuidConverter : JsonConverter<Guid?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var guidStr = reader.GetString();
|
||||
return guidStr == null ? null : new Guid(guidStr);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null || value == Guid.Empty)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStringValue(value.Value.ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a nullable struct or value to/from JSON.
|
||||
/// Required - some clients send an empty string.
|
||||
/// </summary>
|
||||
/// <typeparam name="TStruct">The struct type.</typeparam>
|
||||
public class JsonNullableStructConverter<TStruct> : JsonConverter<TStruct?>
|
||||
where TStruct : struct
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Token is empty string.
|
||||
if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<TStruct>(ref reader, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value.Value, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Json nullable struct converter factory.
|
||||
/// </summary>
|
||||
public class JsonNullableStructConverterFactory : JsonConverterFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return typeToConvert.IsGenericType
|
||||
&& typeToConvert.GetGenericTypeDefinition() == typeof(Nullable<>)
|
||||
&& typeToConvert.GenericTypeArguments[0].IsValueType;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var structType = typeToConvert.GenericTypeArguments[0];
|
||||
return (JsonConverter?)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert Pipe delimited string to array of type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type to convert to.</typeparam>
|
||||
public sealed class JsonPipeDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonPipeDelimitedArrayConverter{T}"/> class.
|
||||
/// </summary>
|
||||
public JsonPipeDelimitedArrayConverter() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override char Delimiter => '|';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Json Pipe delimited array converter factory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
|
||||
/// </remarks>
|
||||
public class JsonPipeDelimitedArrayConverterFactory : JsonConverterFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
|
||||
return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter to allow the serializer to read strings.
|
||||
/// </summary>
|
||||
public class JsonStringConverter : JsonConverter<string?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.Null => null,
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
_ => GetRawValue(reader)
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
|
||||
private static string GetRawValue(Utf8JsonReader reader)
|
||||
{
|
||||
var utf8Bytes = reader.HasValueSequence
|
||||
? reader.ValueSequence.ToArray()
|
||||
: reader.ValueSpan;
|
||||
return Encoding.UTF8.GetString(utf8Bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Extensions.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a Version object or value to/from JSON.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Required to send <see cref="Version"/> as a string instead of an object.
|
||||
/// </remarks>
|
||||
public class JsonVersionConverter : JsonConverter<Version>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> new Version(reader.GetString()!); // Will throw ArgumentNullException on null
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
90
src/Jellyfin.Extensions/Json/JsonDefaults.cs
Normal file
90
src/Jellyfin.Extensions/Json/JsonDefaults.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Extensions.Json.Converters;
|
||||
|
||||
namespace Jellyfin.Extensions.Json
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for having compatible JSON throughout the codebase.
|
||||
/// </summary>
|
||||
public static class JsonDefaults
|
||||
{
|
||||
/// <summary>
|
||||
/// Pascal case json profile media type.
|
||||
/// </summary>
|
||||
public const string PascalCaseMediaType = "application/json; profile=\"PascalCase\"";
|
||||
|
||||
/// <summary>
|
||||
/// Camel case json profile media type.
|
||||
/// </summary>
|
||||
public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\"";
|
||||
|
||||
/// <summary>
|
||||
/// When changing these options, update
|
||||
/// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
|
||||
/// -> AddJellyfinApi
|
||||
/// -> AddJsonOptions.
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions _jsonSerializerOptions = new ()
|
||||
{
|
||||
ReadCommentHandling = JsonCommentHandling.Disallow,
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
Converters =
|
||||
{
|
||||
new JsonGuidConverter(),
|
||||
new JsonNullableGuidConverter(),
|
||||
new JsonVersionConverter(),
|
||||
new JsonStringEnumConverter(),
|
||||
new JsonNullableStructConverterFactory(),
|
||||
new JsonBoolNumberConverter(),
|
||||
new JsonDateTimeConverter(),
|
||||
new JsonStringConverter()
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions _pascalCaseJsonSerializerOptions = new (_jsonSerializerOptions)
|
||||
{
|
||||
PropertyNamingPolicy = null
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions _camelCaseJsonSerializerOptions = new (_jsonSerializerOptions)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default <see cref="JsonSerializerOptions" /> options.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The return value must not be modified.
|
||||
/// If the defaults must be modified the author must use the copy constructor.
|
||||
/// </remarks>
|
||||
/// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
|
||||
public static JsonSerializerOptions Options
|
||||
=> _jsonSerializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets camelCase json options.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The return value must not be modified.
|
||||
/// If the defaults must be modified the author must use the copy constructor.
|
||||
/// </remarks>
|
||||
/// <returns>The camelCase <see cref="JsonSerializerOptions" /> options.</returns>
|
||||
public static JsonSerializerOptions CamelCaseOptions
|
||||
=> _camelCaseJsonSerializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets PascalCase json options.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The return value must not be modified.
|
||||
/// If the defaults must be modified the author must use the copy constructor.
|
||||
/// </remarks>
|
||||
/// <returns>The PascalCase <see cref="JsonSerializerOptions" /> options.</returns>
|
||||
public static JsonSerializerOptions PascalCaseOptions
|
||||
=> _pascalCaseJsonSerializerOptions;
|
||||
}
|
||||
}
|
||||
41
src/Jellyfin.Extensions/ShuffleExtensions.cs
Normal file
41
src/Jellyfin.Extensions/ShuffleExtensions.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides <c>Shuffle</c> extensions methods for <see cref="IList{T}" />.
|
||||
/// </summary>
|
||||
public static class ShuffleExtensions
|
||||
{
|
||||
private static readonly Random _rng = new Random();
|
||||
|
||||
/// <summary>
|
||||
/// Shuffles the items in a list.
|
||||
/// </summary>
|
||||
/// <param name="list">The list that should get shuffled.</param>
|
||||
/// <typeparam name="T">The type.</typeparam>
|
||||
public static void Shuffle<T>(this IList<T> list)
|
||||
{
|
||||
list.Shuffle(_rng);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuffles the items in a list.
|
||||
/// </summary>
|
||||
/// <param name="list">The list that should get shuffled.</param>
|
||||
/// <param name="rng">The random number generator to use.</param>
|
||||
/// <typeparam name="T">The type.</typeparam>
|
||||
public static void Shuffle<T>(this IList<T> list, Random rng)
|
||||
{
|
||||
int n = list.Count;
|
||||
while (n > 1)
|
||||
{
|
||||
int k = rng.Next(n--);
|
||||
T value = list[k];
|
||||
list[k] = list[n];
|
||||
list[n] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/Jellyfin.Extensions/SplitStringExtensions.cs
Normal file
115
src/Jellyfin.Extensions/SplitStringExtensions.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Gérald Barré
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
// TODO: remove when analyzer is fixed: https://github.com/dotnet/roslyn-analyzers/issues/5158
|
||||
#pragma warning disable CA1034 // Nested types should not be visible
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Jellyfin.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension class for splitting lines without unnecessary allocations.
|
||||
/// </summary>
|
||||
public static class SplitStringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new string split enumerator.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to split.</param>
|
||||
/// <param name="separator">The separator to split on.</param>
|
||||
/// <returns>The enumerator struct.</returns>
|
||||
[Pure]
|
||||
public static Enumerator SpanSplit(this string str, char separator) => new (str.AsSpan(), separator);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new span split enumerator.
|
||||
/// </summary>
|
||||
/// <param name="str">The span to split.</param>
|
||||
/// <param name="separator">The separator to split on.</param>
|
||||
/// <returns>The enumerator struct.</returns>
|
||||
[Pure]
|
||||
public static Enumerator Split(this ReadOnlySpan<char> str, char separator) => new (str, separator);
|
||||
|
||||
/// <summary>
|
||||
/// Provides an enumerator for the substrings seperated by the separator.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public ref struct Enumerator
|
||||
{
|
||||
private readonly char _separator;
|
||||
private ReadOnlySpan<char> _str;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Enumerator"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="str">The span to split.</param>
|
||||
/// <param name="separator">The separator to split on.</param>
|
||||
public Enumerator(ReadOnlySpan<char> str, char separator)
|
||||
{
|
||||
_str = str;
|
||||
_separator = separator;
|
||||
Current = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a reference to the item at the current position of the enumerator.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<char> Current { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>this</c>.
|
||||
/// </summary>
|
||||
/// <returns><c>this</c>.</returns>
|
||||
public readonly Enumerator GetEnumerator() => this;
|
||||
|
||||
/// <summary>
|
||||
/// Advances the enumerator to the next item.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if there is a next element; otherwise <c>false</c>.</returns>
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_str.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var span = _str;
|
||||
var index = span.IndexOf(_separator);
|
||||
if (index == -1)
|
||||
{
|
||||
_str = ReadOnlySpan<char>.Empty;
|
||||
Current = span;
|
||||
return true;
|
||||
}
|
||||
|
||||
Current = span.Slice(0, index);
|
||||
_str = span[(index + 1)..];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/Jellyfin.Extensions/StreamExtensions.cs
Normal file
63
src/Jellyfin.Extensions/StreamExtensions.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Jellyfin.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BaseExtensions.
|
||||
/// </summary>
|
||||
public static class StreamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads all lines in the <see cref="Stream" />.
|
||||
/// </summary>
|
||||
/// <param name="stream">The <see cref="Stream" /> to read from.</param>
|
||||
/// <returns>All lines in the stream.</returns>
|
||||
public static string[] ReadAllLines(this Stream stream)
|
||||
=> ReadAllLines(stream, Encoding.UTF8);
|
||||
|
||||
/// <summary>
|
||||
/// Reads all lines in the <see cref="Stream" />.
|
||||
/// </summary>
|
||||
/// <param name="stream">The <see cref="Stream" /> to read from.</param>
|
||||
/// <param name="encoding">The character encoding to use.</param>
|
||||
/// <returns>All lines in the stream.</returns>
|
||||
public static string[] ReadAllLines(this Stream stream, Encoding encoding)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(stream, encoding))
|
||||
{
|
||||
return ReadAllLines(reader).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all lines in the <see cref="TextReader" />.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="TextReader" /> to read from.</param>
|
||||
/// <returns>All lines in the stream.</returns>
|
||||
public static IEnumerable<string> ReadAllLines(this TextReader reader)
|
||||
{
|
||||
string? line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
yield return line;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all lines in the <see cref="TextReader" />.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="TextReader" /> to read from.</param>
|
||||
/// <returns>All lines in the stream.</returns>
|
||||
public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader)
|
||||
{
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
|
||||
{
|
||||
yield return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Jellyfin.Extensions/StringBuilderExtensions.cs
Normal file
35
src/Jellyfin.Extensions/StringBuilderExtensions.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Jellyfin.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="StringBuilder"/> class.
|
||||
/// </summary>
|
||||
public static class StringBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Concatenates and appends the members of a collection in single quotes using the specified delimiter.
|
||||
/// </summary>
|
||||
/// <param name="builder">The string builder.</param>
|
||||
/// <param name="delimiter">The character delimiter.</param>
|
||||
/// <param name="values">The collection of strings to concatenate.</param>
|
||||
/// <returns>The updated string builder.</returns>
|
||||
public static StringBuilder AppendJoinInSingleQuotes(this StringBuilder builder, char delimiter, IReadOnlyList<string> values)
|
||||
{
|
||||
var len = values.Count;
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
builder.Append('\'')
|
||||
.Append(values[i])
|
||||
.Append('\'')
|
||||
.Append(delimiter);
|
||||
}
|
||||
|
||||
// remove last ,
|
||||
builder.Length--;
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/Jellyfin.Extensions/StringExtensions.cs
Normal file
31
src/Jellyfin.Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extensions methods for <see cref="string" />.
|
||||
/// </summary>
|
||||
public static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Counts the number of occurrences of [needle] in the string.
|
||||
/// </summary>
|
||||
/// <param name="value">The haystack to search in.</param>
|
||||
/// <param name="needle">The character to search for.</param>
|
||||
/// <returns>The number of occurrences of the [needle] character.</returns>
|
||||
public static int Count(this ReadOnlySpan<char> value, char needle)
|
||||
{
|
||||
var count = 0;
|
||||
var length = value.Length;
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
if (value[i] == needle)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user