using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; namespace Jellyfin.Plugin.Floatplane.Api.Models; /// Image with optional pre-scaled variants (pbs.floatplane.com, public). public class ImageModel { public int Width { get; set; } public int Height { get; set; } public string Path { get; set; } = string.Empty; public List ChildImages { get; set; } = new(); /// Largest variant whose width does not exceed , else the original. public string BestPath(int maxWidth = 1200) { var candidates = ChildImages.Where(c => c.Width <= maxWidth && !string.IsNullOrEmpty(c.Path)).OrderByDescending(c => c.Width).ToList(); if (candidates.Count > 0) { return candidates[0].Path; } return Path; } } /// Error envelope Floatplane returns for 4xx responses. public class ApiErrorModel { public string? Id { get; set; } public List Errors { get; set; } = new(); public string? Message { get; set; } public bool Has(string name) => Errors.Any(e => string.Equals(e.Name, name, StringComparison.OrdinalIgnoreCase)); } public class ApiErrorItem { public string? Id { get; set; } public string? Name { get; set; } public string? Message { get; set; } } /// /// Floatplane sometimes sends a related object inline and sometimes only its id /// (e.g. channel on a post, videoAttachments in list responses). /// This converter accepts both: a string becomes { Id = string }. /// /// Model type with an Id property. public class IdOrObjectConverter : JsonConverter where T : class, IHasId, new() { public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) { return null; } if (reader.TokenType == JsonTokenType.String) { return new T { Id = reader.GetString() ?? string.Empty, IsReferenceOnly = true }; } // Avoid infinite recursion: deserialise with a copy of the options minus this converter. var inner = new JsonSerializerOptions(options); var self = inner.Converters.FirstOrDefault(c => c is IdOrObjectConverter); if (self != null) { inner.Converters.Remove(self); } return JsonSerializer.Deserialize(ref reader, inner); } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { var inner = new JsonSerializerOptions(options); var self = inner.Converters.FirstOrDefault(c => c is IdOrObjectConverter); if (self != null) { inner.Converters.Remove(self); } JsonSerializer.Serialize(writer, value, inner); } } public interface IHasId { string Id { get; set; } /// True when only the id was present in the JSON. bool IsReferenceOnly { get; set; } } /// Shared serializer options for Floatplane JSON. public static class FloatplaneJson { public static JsonSerializerOptions Options { get; } = Create(); private static JsonSerializerOptions Create() { var o = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; o.Converters.Add(new IdOrObjectConverter()); o.Converters.Add(new IdOrObjectConverter()); o.Converters.Add(new IdOrObjectConverter()); o.Converters.Add(new JsonStringEnumConverter()); return o; } }