Floatplane channel plugin for Jellyfin 10.10.x: browse, HLS proxy, cookie session, CI release
Some checks failed
release / build-release (push) Has been cancelled
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>
This commit is contained in:
177
Jellyfin.Plugin.Floatplane/Api/Models/Delivery.cs
Normal file
177
Jellyfin.Plugin.Floatplane/Api/Models/Delivery.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user