using System; using System.Collections.Generic; using System.Linq; namespace Jellyfin.Plugin.Floatplane.Api.Models; /// GET /api/v3/delivery/info response. public class DeliveryInfoModel { public List Groups { get; set; } = new(); /// Flattens every usable variant with its absolute URL. public IEnumerable Resolve() { foreach (var group in Groups) { var groupOrigin = group.Origins.FirstOrDefault()?.Url; foreach (var v in group.Variants) { var origin = v.Origins?.FirstOrDefault()?.Url ?? groupOrigin; if (string.IsNullOrEmpty(v.Url)) { continue; } string url; if (IsHttpUrl(v.Url)) { url = v.Url; } else if (!string.IsNullOrEmpty(origin)) { url = origin.TrimEnd('/') + "/" + v.Url.TrimStart('/'); } else { continue; } yield return new ResolvedVariant(v, url); } } } /// /// True for http(s) URLs only. Uri.TryCreate(…, UriKind.Absolute) must not be used /// for this: on Linux a path like /Videos/x.m3u8 parses as an absolute file:/// URI. /// public static bool IsHttpUrl(string value) => value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || value.StartsWith("https://", StringComparison.OrdinalIgnoreCase); } public class DeliveryGroup { public List Origins { get; set; } = new(); public List Variants { get; set; } = new(); } public class DeliveryOrigin { public string Url { get; set; } = string.Empty; public string? QueryUrl { get; set; } } public class DeliveryVariant { /// e.g. "1080-avc1". public string Name { get; set; } = string.Empty; /// e.g. "1080p", "4K". public string? Label { get; set; } /// Relative (to origin) or absolute URL incl. signed token. Empty when denied. public string Url { get; set; } = string.Empty; public string? MimeType { get; set; } public long Order { get; set; } public bool Hidden { get; set; } public bool Enabled { get; set; } = true; public List? Origins { get; set; } public VariantMeta? Meta { get; set; } public bool IsUsable => Enabled && !Hidden && !string.IsNullOrEmpty(Url); public int Height => Meta?.Video?.Height ?? ParseHeight(Name); public static int ParseHeight(string name) { var dash = name.IndexOf('-', StringComparison.Ordinal); var head = dash > 0 ? name[..dash] : name; return int.TryParse(head, out var h) ? h : 0; } } public class VariantMeta { public VariantVideoMeta? Video { get; set; } public VariantAudioMeta? Audio { get; set; } public VariantCommonMeta? Common { get; set; } public VariantLiveMeta? Live { get; set; } } public class VariantVideoMeta { public string? Codec { get; set; } public string? CodecSimple { get; set; } public BitrateMeta? Bitrate { get; set; } public int Width { get; set; } public int Height { get; set; } public bool IsHdr { get; set; } public double Fps { get; set; } public string? MimeType { get; set; } } public class VariantAudioMeta { public string? Codec { get; set; } public BitrateMeta? Bitrate { get; set; } public int ChannelCount { get; set; } public int Samplerate { get; set; } public string? MimeType { get; set; } } public class BitrateMeta { public long Average { get; set; } public long? Maximum { get; set; } } public class VariantCommonMeta { public VariantAccess? Access { get; set; } } public class VariantAccess { public string? DeniedReason { get; set; } public string? DeniedMessage { get; set; } } public class VariantLiveMeta { public string? LowLatencyExtension { get; set; } } /// A delivery variant with its absolute URL. public record ResolvedVariant(DeliveryVariant Variant, string Url) { public string Name => Variant.Name; public int Height => Variant.Height; public bool IsUsable => Variant.IsUsable; }