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:
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Jellyfin.Plugin.Chaosflix.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Reads back a proxy url the channel handed out, so a diagnostic can check it and show
|
||||
/// it without handing the reader a url they could stream through.
|
||||
/// </summary>
|
||||
internal static class ProxyUrl
|
||||
{
|
||||
private const string FolderParameter = "recordingFolder";
|
||||
private const string LanguageParameter = "language";
|
||||
|
||||
/// <summary>
|
||||
/// Renders the url without its signature. The result is rebuilt from the parts that
|
||||
/// were understood rather than edited in place, so nothing of the signature can
|
||||
/// survive a url shaped differently than expected.
|
||||
/// </summary>
|
||||
internal static string Redact(string url)
|
||||
{
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return "<unparseable proxy url>";
|
||||
}
|
||||
|
||||
var query = Parse(uri);
|
||||
var parts = new List<string>();
|
||||
if (query.TryGetValue(FolderParameter, out var folder))
|
||||
{
|
||||
parts.Add($"{FolderParameter}={folder}");
|
||||
}
|
||||
|
||||
if (query.TryGetValue(LanguageParameter, out var language))
|
||||
{
|
||||
parts.Add($"{LanguageParameter}={language}");
|
||||
}
|
||||
|
||||
parts.Add($"{ProxySignature.QueryParameter}=<redacted>");
|
||||
return $"{uri.GetLeftPart(UriPartial.Path)}?{string.Join('&', parts)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the url carries a signature the proxy endpoint would accept for exactly
|
||||
/// the recording the url names.
|
||||
/// </summary>
|
||||
internal static bool IsSignedCorrectly(string url)
|
||||
{
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var guid = uri.Segments.LastOrDefault()?.TrimEnd('/');
|
||||
if (string.IsNullOrEmpty(guid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var query = Parse(uri);
|
||||
query.TryGetValue(ProxySignature.QueryParameter, out var signature);
|
||||
return ProxySignature.Verify(
|
||||
Uri.UnescapeDataString(guid),
|
||||
query.GetValueOrDefault(FolderParameter),
|
||||
query.GetValueOrDefault(LanguageParameter),
|
||||
signature);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> Parse(Uri uri)
|
||||
{
|
||||
var query = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var pair in uri.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var separator = pair.IndexOf('=', StringComparison.Ordinal);
|
||||
if (separator > 0)
|
||||
{
|
||||
query[pair[..separator]] = Uri.UnescapeDataString(pair[(separator + 1)..]);
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user