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>
95 lines
3.9 KiB
C#
95 lines
3.9 KiB
C#
using System;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace Jellyfin.Plugin.Floatplane.Playback;
|
|
|
|
/// <summary>
|
|
/// Rewrites an HLS playlist so that (a) every media/segment URI is absolute and
|
|
/// (b) the AES-128 key URI (which needs the Floatplane session cookie) and any nested
|
|
/// playlist URIs point back at the plugin's proxy.
|
|
/// </summary>
|
|
public static partial class HlsRewriter
|
|
{
|
|
[GeneratedRegex("URI=\"([^\"]*)\"", RegexOptions.CultureInvariant)]
|
|
private static partial Regex UriAttribute();
|
|
|
|
/// <param name="playlist">Playlist text as served by the CDN.</param>
|
|
/// <param name="playlistUrl">Absolute URL the playlist was fetched from (base for relative URIs).</param>
|
|
/// <param name="mapKeyUri">Maps the absolute key URI to the proxy key URL.</param>
|
|
/// <param name="mapPlaylistUri">Maps a nested playlist URI (master playlists) to a proxy URL; null = leave absolute.</param>
|
|
/// <param name="mapSegmentUri">Maps a segment URI; null = leave absolute (segments need no cookie).</param>
|
|
public static string Rewrite(
|
|
string playlist,
|
|
Uri playlistUrl,
|
|
Func<string, string> mapKeyUri,
|
|
Func<string, string>? mapPlaylistUri = null,
|
|
Func<string, string>? mapSegmentUri = null)
|
|
{
|
|
var sb = new StringBuilder(playlist.Length + 512);
|
|
var isMaster = playlist.Contains("#EXT-X-STREAM-INF", StringComparison.Ordinal);
|
|
var nextIsVariantPlaylist = false;
|
|
|
|
foreach (var rawLine in playlist.Split('\n'))
|
|
{
|
|
var line = rawLine.TrimEnd('\r');
|
|
if (line.Length == 0)
|
|
{
|
|
sb.Append('\n');
|
|
continue;
|
|
}
|
|
|
|
if (line.StartsWith('#'))
|
|
{
|
|
if (line.StartsWith("#EXT-X-KEY", StringComparison.Ordinal) || line.StartsWith("#EXT-X-SESSION-KEY", StringComparison.Ordinal))
|
|
{
|
|
line = UriAttribute().Replace(line, m => "URI=\"" + mapKeyUri(Absolute(playlistUrl, m.Groups[1].Value)) + "\"", 1);
|
|
}
|
|
else if (line.StartsWith("#EXT-X-MAP", StringComparison.Ordinal))
|
|
{
|
|
line = UriAttribute().Replace(line, m => "URI=\"" + MapSegment(Absolute(playlistUrl, m.Groups[1].Value), mapSegmentUri) + "\"", 1);
|
|
}
|
|
else if (line.StartsWith("#EXT-X-MEDIA", StringComparison.Ordinal) && mapPlaylistUri != null)
|
|
{
|
|
line = UriAttribute().Replace(line, m => "URI=\"" + mapPlaylistUri(Absolute(playlistUrl, m.Groups[1].Value)) + "\"", 1);
|
|
}
|
|
else if (line.StartsWith("#EXT-X-STREAM-INF", StringComparison.Ordinal))
|
|
{
|
|
nextIsVariantPlaylist = true;
|
|
}
|
|
|
|
sb.Append(line).Append('\n');
|
|
continue;
|
|
}
|
|
|
|
var absolute = Absolute(playlistUrl, line);
|
|
if (isMaster && nextIsVariantPlaylist)
|
|
{
|
|
nextIsVariantPlaylist = false;
|
|
sb.Append(mapPlaylistUri != null ? mapPlaylistUri(absolute) : absolute).Append('\n');
|
|
}
|
|
else
|
|
{
|
|
sb.Append(MapSegment(absolute, mapSegmentUri)).Append('\n');
|
|
}
|
|
}
|
|
|
|
return sb.ToString().TrimEnd('\n') + "\n";
|
|
}
|
|
|
|
private static string MapSegment(string absolute, Func<string, string>? mapSegmentUri)
|
|
=> mapSegmentUri != null ? mapSegmentUri(absolute) : absolute;
|
|
|
|
/// <summary>Resolves <paramref name="reference"/> against <paramref name="baseUrl"/> (RFC 3986).</summary>
|
|
public static string Absolute(Uri baseUrl, string reference)
|
|
{
|
|
// Only http(s) is "absolute" here: on Linux Uri.TryCreate("/x.ts", Absolute) yields file:///x.ts.
|
|
if (Api.Models.DeliveryInfoModel.IsHttpUrl(reference))
|
|
{
|
|
return reference;
|
|
}
|
|
|
|
return new Uri(baseUrl, reference).ToString();
|
|
}
|
|
}
|