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>
178 lines
4.4 KiB
C#
178 lines
4.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Jellyfin.Plugin.Floatplane.Api.Models;
|
|
|
|
/// <summary>GET /api/v3/delivery/info response.</summary>
|
|
public class DeliveryInfoModel
|
|
{
|
|
public List<DeliveryGroup> Groups { get; set; } = new();
|
|
|
|
/// <summary>Flattens every usable variant with its absolute URL.</summary>
|
|
public IEnumerable<ResolvedVariant> 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// True for http(s) URLs only. <c>Uri.TryCreate(…, UriKind.Absolute)</c> must not be used
|
|
/// for this: on Linux a path like <c>/Videos/x.m3u8</c> parses as an absolute <c>file:///</c> URI.
|
|
/// </summary>
|
|
public static bool IsHttpUrl(string value)
|
|
=> value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || value.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public class DeliveryGroup
|
|
{
|
|
public List<DeliveryOrigin> Origins { get; set; } = new();
|
|
|
|
public List<DeliveryVariant> Variants { get; set; } = new();
|
|
}
|
|
|
|
public class DeliveryOrigin
|
|
{
|
|
public string Url { get; set; } = string.Empty;
|
|
|
|
public string? QueryUrl { get; set; }
|
|
}
|
|
|
|
public class DeliveryVariant
|
|
{
|
|
/// <summary>e.g. "1080-avc1".</summary>
|
|
public string Name { get; set; } = string.Empty;
|
|
|
|
/// <summary>e.g. "1080p", "4K".</summary>
|
|
public string? Label { get; set; }
|
|
|
|
/// <summary>Relative (to origin) or absolute URL incl. signed token. Empty when denied.</summary>
|
|
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<DeliveryOrigin>? 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; }
|
|
}
|
|
|
|
/// <summary>A delivery variant with its absolute URL.</summary>
|
|
public record ResolvedVariant(DeliveryVariant Variant, string Url)
|
|
{
|
|
public string Name => Variant.Name;
|
|
|
|
public int Height => Variant.Height;
|
|
|
|
public bool IsUsable => Variant.IsUsable;
|
|
}
|