Files
jellyfin-plugin-floatplane/Jellyfin.Plugin.Floatplane/Api/Models/Delivery.cs
claude 9e33a343ab
Some checks failed
release / build-release (push) Has been cancelled
Floatplane channel plugin for Jellyfin 10.10.x: browse, HLS proxy, cookie session, CI release
- 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>
2026-10-25 09:42:27 +01:00

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;
}