using System;
using System.Text;
using System.Text.RegularExpressions;
namespace Jellyfin.Plugin.Floatplane.Playback;
///
/// 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.
///
public static partial class HlsRewriter
{
[GeneratedRegex("URI=\"([^\"]*)\"", RegexOptions.CultureInvariant)]
private static partial Regex UriAttribute();
/// Playlist text as served by the CDN.
/// Absolute URL the playlist was fetched from (base for relative URIs).
/// Maps the absolute key URI to the proxy key URL.
/// Maps a nested playlist URI (master playlists) to a proxy URL; null = leave absolute.
/// Maps a segment URI; null = leave absolute (segments need no cookie).
public static string Rewrite(
string playlist,
Uri playlistUrl,
Func mapKeyUri,
Func? mapPlaylistUri = null,
Func? 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? mapSegmentUri)
=> mapSegmentUri != null ? mapSegmentUri(absolute) : absolute;
/// Resolves against (RFC 3986).
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();
}
}