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