Some checks failed
release / build-release (push) Has been cancelled
- IChannel: creators -> sub-channels -> video posts (public listing API, search mapped) - IRequiresMediaInfoCallback -> signed proxy URLs; stream controller rewrites HLS so the AES-128 key is fetched server-side with the session cookie, segments stay on the CDN - delivery/info cached per token lifetime and throttled (~2/min) - config page: paste sails.sid + validate, extra creators, quality, search test - scripts/package.sh -> plugin folder + zip + md5 + manifest.json; Gitea release workflow - research docs, HAR analysis (HAR itself gitignored), install-pi5.sh Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
128 lines
4.0 KiB
C#
128 lines
4.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace Jellyfin.Plugin.Floatplane.Api.Models;
|
|
|
|
/// <summary>Image with optional pre-scaled variants (<c>pbs.floatplane.com</c>, public).</summary>
|
|
public class ImageModel
|
|
{
|
|
public int Width { get; set; }
|
|
|
|
public int Height { get; set; }
|
|
|
|
public string Path { get; set; } = string.Empty;
|
|
|
|
public List<ImageModel> ChildImages { get; set; } = new();
|
|
|
|
/// <summary>Largest variant whose width does not exceed <paramref name="maxWidth"/>, else the original.</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Error envelope Floatplane returns for 4xx responses.</summary>
|
|
public class ApiErrorModel
|
|
{
|
|
public string? Id { get; set; }
|
|
|
|
public List<ApiErrorItem> 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; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Floatplane sometimes sends a related object inline and sometimes only its id
|
|
/// (e.g. <c>channel</c> on a post, <c>videoAttachments</c> in list responses).
|
|
/// This converter accepts both: a string becomes <c>{ Id = string }</c>.
|
|
/// </summary>
|
|
/// <typeparam name="T">Model type with an <c>Id</c> property.</typeparam>
|
|
public class IdOrObjectConverter<T> : JsonConverter<T>
|
|
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<T>);
|
|
if (self != null)
|
|
{
|
|
inner.Converters.Remove(self);
|
|
}
|
|
|
|
return JsonSerializer.Deserialize<T>(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<T>);
|
|
if (self != null)
|
|
{
|
|
inner.Converters.Remove(self);
|
|
}
|
|
|
|
JsonSerializer.Serialize(writer, value, inner);
|
|
}
|
|
}
|
|
|
|
public interface IHasId
|
|
{
|
|
string Id { get; set; }
|
|
|
|
/// <summary>True when only the id was present in the JSON.</summary>
|
|
bool IsReferenceOnly { get; set; }
|
|
}
|
|
|
|
/// <summary>Shared serializer options for Floatplane JSON.</summary>
|
|
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<ChannelModel>());
|
|
o.Converters.Add(new IdOrObjectConverter<CreatorModel>());
|
|
o.Converters.Add(new IdOrObjectConverter<VideoModel>());
|
|
o.Converters.Add(new JsonStringEnumConverter());
|
|
return o;
|
|
}
|
|
}
|