repo creation with initial code after cloning public repo

This commit is contained in:
2026-02-19 07:36:25 -05:00
commit 460884fea3
2860 changed files with 650825 additions and 0 deletions
@@ -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,33 @@
using System;
using System.Buffers;
using System.Buffers.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters;
/// <summary>
/// Converts a string to a boolean.
/// This is needed for FFprobe.
/// </summary>
public class JsonBoolStringConverter : JsonConverter<bool>
{
/// <inheritdoc />
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
ReadOnlySpan<byte> utf8Span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
if (Utf8Parser.TryParse(utf8Span, out bool val, out _, 'l'))
{
return val;
}
}
return reader.GetBoolean();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
=> writer.WriteBooleanValue(value);
}
@@ -0,0 +1,19 @@
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Convert comma delimited string to collection of type.
/// </summary>
/// <typeparam name="T">Type to convert to.</typeparam>
public sealed class JsonCommaDelimitedCollectionConverter<T> : JsonDelimitedCollectionConverter<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonCommaDelimitedCollectionConverter{T}"/> class.
/// </summary>
public JsonCommaDelimitedCollectionConverter() : base()
{
}
/// <inheritdoc />
protected override char Delimiter => ',';
}
}
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Json comma delimited collection converter factory.
/// </summary>
/// <remarks>
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
/// </remarks>
public class JsonCommaDelimitedCollectionConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsArray
|| (typeToConvert.IsGenericType
&& (typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyCollection<>)) || typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyList<>))));
}
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedCollectionConverter<>).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,49 @@
using System;
using System.ComponentModel;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters;
/// <summary>
/// Json unknown enum converter.
/// </summary>
/// <typeparam name="T">The type of enum.</typeparam>
public class JsonDefaultStringEnumConverter<T> : JsonConverter<T>
where T : struct, Enum
{
private readonly JsonConverter<T> _baseConverter;
/// <summary>
/// Initializes a new instance of the <see cref="JsonDefaultStringEnumConverter{T}"/> class.
/// </summary>
/// <param name="baseConverter">The base json converter.</param>
public JsonDefaultStringEnumConverter(JsonConverter<T> baseConverter)
{
_baseConverter = baseConverter;
}
/// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.IsNull() || reader.IsEmptyString())
{
var customValueAttribute = typeToConvert.GetCustomAttribute<DefaultValueAttribute>();
if (customValueAttribute?.Value is null)
{
throw new InvalidOperationException($"Default value not set for '{typeToConvert.Name}'");
}
return (T)customValueAttribute.Value;
}
return _baseConverter.Read(ref reader, typeToConvert, options);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
_baseConverter.Write(writer, value, options);
}
}
@@ -0,0 +1,31 @@
using System;
using System.ComponentModel;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters;
/// <summary>
/// Utilizes the JsonStringEnumConverter and sets a default value if not provided.
/// </summary>
public class JsonDefaultStringEnumConverterFactory : JsonConverterFactory
{
private static readonly JsonStringEnumConverter _baseConverterFactory = new();
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return _baseConverterFactory.CanConvert(typeToConvert)
&& typeToConvert.IsDefined(typeof(DefaultValueAttribute));
}
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var baseConverter = _baseConverterFactory.CreateConverter(typeToConvert, options);
var converterType = typeof(JsonDefaultStringEnumConverter<>).MakeGenericType(typeToConvert);
return (JsonConverter?)Activator.CreateInstance(converterType, baseConverter);
}
}
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
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 JsonDelimitedCollectionConverter<T> : JsonConverter<IReadOnlyCollection<T>>
{
private readonly TypeConverter _typeConverter;
/// <summary>
/// Initializes a new instance of the <see cref="JsonDelimitedCollectionConverter{T}"/> class.
/// </summary>
protected JsonDelimitedCollectionConverter()
{
_typeConverter = TypeDescriptor.GetConverter(typeof(T));
}
/// <summary>
/// Gets the array delimiter.
/// </summary>
protected virtual char Delimiter { get; }
/// <inheritdoc />
public override IReadOnlyCollection<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
// null got handled higher up the call stack
var stringEntries = reader.GetString()!.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries);
if (stringEntries.Length == 0)
{
return [];
}
var typedValues = new List<T>();
for (var i = 0; i < stringEntries.Length; i++)
{
try
{
var parsedValue = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim());
if (parsedValue is not null)
{
typedValues.Add((T)parsedValue);
}
}
catch (FormatException)
{
// Ignore unconvertible inputs
}
}
if (typeToConvert.IsArray)
{
return typedValues.ToArray();
}
return typedValues;
}
return JsonSerializer.Deserialize<T[]>(ref reader, options);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, IReadOnlyCollection<T>? value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}
}
@@ -0,0 +1,36 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters;
/// <summary>
/// Enum flag to json array converter.
/// </summary>
/// <typeparam name="T">The type of enum.</typeparam>
public class JsonFlagEnumConverter<T> : JsonConverter<T>
where T : struct, Enum
{
private static readonly T[] _enumValues = Enum.GetValues<T>();
/// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var enumValue in _enumValues)
{
if (value.HasFlag(enumValue))
{
writer.WriteStringValue(enumValue.ToString());
}
}
writer.WriteEndArray();
}
}
@@ -0,0 +1,24 @@
using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters;
/// <summary>
/// Json flag enum converter factory.
/// </summary>
public class JsonFlagEnumConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsEnum && typeToConvert.IsDefined(typeof(FlagsAttribute));
}
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return (JsonConverter?)Activator.CreateInstance(typeof(JsonFlagEnumConverter<>).MakeGenericType(typeToConvert));
}
}
@@ -0,0 +1,30 @@
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)
=> reader.IsNull()
? Guid.Empty
: ReadInternal(ref reader);
// TODO: optimize by parsing the UTF8 bytes instead of converting to string first
internal static Guid ReadInternal(ref Utf8JsonReader reader)
=> Guid.Parse(reader.GetString()!); // null got handled higher up the call stack
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
=> WriteInternal(writer, value);
internal static void WriteInternal(Utf8JsonWriter writer, Guid value)
=> writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture));
}
}
@@ -0,0 +1,31 @@
using System;
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)
=> JsonGuidConverter.ReadInternal(ref reader);
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options)
{
// null got handled higher up the call stack
var val = value!.Value;
if (val.IsEmpty())
{
writer.WriteNullValue();
}
else
{
JsonGuidConverter.WriteInternal(writer, val);
}
}
}
}
@@ -0,0 +1,30 @@
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.IsEmptyString())
{
return null;
}
return JsonSerializer.Deserialize<TStruct>(ref reader, options);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options)
=> JsonSerializer.Serialize(writer, value!.Value, options); // null got handled higher up the call stack
}
}
@@ -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,19 @@
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 JsonPipeDelimitedCollectionConverter<T> : JsonDelimitedCollectionConverter<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonPipeDelimitedCollectionConverter{T}"/> class.
/// </summary>
public JsonPipeDelimitedCollectionConverter() : base()
{
}
/// <inheritdoc />
protected override char Delimiter => '|';
}
}
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Json Pipe delimited collection converter factory.
/// </summary>
/// <remarks>
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
/// </remarks>
public class JsonPipeDelimitedCollectionConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsArray
|| (typeToConvert.IsGenericType
&& (typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyCollection<>)) || typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyList<>))));
}
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedCollectionConverter<>).MakeGenericType(structType));
}
}
}
@@ -0,0 +1,30 @@
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)
=> reader.TokenType == 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());
}
}
@@ -0,0 +1,93 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
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 JsonFlagEnumConverterFactory(),
new JsonDefaultStringEnumConverterFactory(),
new JsonStringEnumConverter(),
new JsonNullableStructConverterFactory(),
new JsonDateTimeConverter(),
new JsonStringConverter()
},
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
};
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;
}
}
@@ -0,0 +1,27 @@
using System.Text.Json;
namespace Jellyfin.Extensions.Json;
/// <summary>
/// Extensions for Utf8JsonReader and Utf8JsonWriter.
/// </summary>
public static class Utf8JsonExtensions
{
/// <summary>
/// Determines if the reader contains an empty string.
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>Whether the reader contains an empty string.</returns>
public static bool IsEmptyString(this Utf8JsonReader reader)
=> reader.TokenType == JsonTokenType.String
&& ((reader.HasValueSequence && reader.ValueSequence.IsEmpty)
|| (!reader.HasValueSequence && reader.ValueSpan.IsEmpty));
/// <summary>
/// Determines if the reader contains a null value.
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>Whether the reader contains null.</returns>
public static bool IsNull(this Utf8JsonReader reader)
=> reader.TokenType == JsonTokenType.Null;
}