using System; using System.Linq; using Jellyfin.Plugin.Floatplane.Playback; using Xunit; namespace Jellyfin.Plugin.Floatplane.Tests; public class HlsRewriterTests { private static readonly Uri PlaylistUrl = new("https://cdn-vod-drm2.floatplane.com/Videos/dcm2jUIg1J/1080.mp4/chunk.m3u8?token=X"); [Fact] public void Media_playlist_key_is_proxied_and_segments_made_absolute() { var original = Fixture.Text("chunk.m3u8"); string? seenKey = null; var rewritten = HlsRewriter.Rewrite( original, PlaylistUrl, key => { seenKey = key; return "http://jellyfin/Floatplane/Stream/dcm2jUIg1J/Key?u=abc&t=sig"; }); var lines = rewritten.Split('\n', StringSplitOptions.RemoveEmptyEntries); Assert.Equal("https://www.floatplane.com/api/video/watchKey?token=eyJwYXRoIjoiL1ZpZGVvcy9kY20yalVJZzFKLzEwODAubXA0IiwidGltZXN0YW1wIjoxNzkyODg2NDAwMDAwfQ", seenKey); Assert.Contains("#EXT-X-KEY:METHOD=AES-128,URI=\"http://jellyfin/Floatplane/Stream/dcm2jUIg1J/Key?u=abc&t=sig\"", lines); Assert.DoesNotContain(lines, l => l.Contains("watchKey", StringComparison.Ordinal)); var segments = lines.Where(l => !l.StartsWith('#')).ToList(); Assert.Equal(6, segments.Count); for (var i = 0; i < segments.Count; i++) { Assert.StartsWith("https://cdn-vod-drm2.floatplane.com/Videos/dcm2jUIg1J/1080.mp4/", segments[i]); Assert.EndsWith($".ts?token=SEGTOKEN{i + 1}", segments[i]); } Assert.Equal(6, lines.Count(l => l.StartsWith("#EXTINF:", StringComparison.Ordinal))); Assert.Equal("#EXTM3U", lines[0]); Assert.Equal("#EXT-X-ENDLIST", lines[^1]); Assert.Contains("#EXT-X-TARGETDURATION:12", lines); Assert.EndsWith("\n", rewritten); } [Fact] public void Segment_mapper_is_applied_when_given() { var rewritten = HlsRewriter.Rewrite( Fixture.Text("chunk.m3u8"), PlaylistUrl, key => "KEY", mapSegmentUri: seg => "http://proxy/seg?u=" + Uri.EscapeDataString(seg)); var segments = rewritten.Split('\n', StringSplitOptions.RemoveEmptyEntries).Where(l => !l.StartsWith('#')).ToList(); Assert.Equal(6, segments.Count); Assert.All(segments, s => Assert.StartsWith("http://proxy/seg?u=https%3A%2F%2Fcdn-vod-drm2.floatplane.com", s)); } [Fact] public void Master_playlist_variants_are_mapped_through_playlist_mapper() { const string master = "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1800000,RESOLUTION=1920x1080\n1080.mp4/chunk.m3u8?token=A\n#EXT-X-STREAM-INF:BANDWIDTH=900000,RESOLUTION=1280x720\nhttps://other.cdn/720/chunk.m3u8?token=B\n"; var masterUrl = new Uri("https://cdn-vod-drm2.floatplane.com/Videos/dcm2jUIg1J/master.m3u8?token=M"); var rewritten = HlsRewriter.Rewrite( master, masterUrl, key => "KEY", playlistUri => "http://proxy/pl?u=" + playlistUri); var lines = rewritten.Split('\n', StringSplitOptions.RemoveEmptyEntries); Assert.Equal("http://proxy/pl?u=https://cdn-vod-drm2.floatplane.com/Videos/dcm2jUIg1J/1080.mp4/chunk.m3u8?token=A", lines[2]); Assert.Equal("http://proxy/pl?u=https://other.cdn/720/chunk.m3u8?token=B", lines[4]); Assert.Equal("#EXT-X-STREAM-INF:BANDWIDTH=1800000,RESOLUTION=1920x1080", lines[1]); } [Fact] public void Master_playlist_without_mapper_keeps_absolute_urls() { const string master = "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\nsub/chunk.m3u8\n"; var rewritten = HlsRewriter.Rewrite(master, new Uri("https://cdn.example/dir/master.m3u8"), key => "KEY"); Assert.Contains("https://cdn.example/dir/sub/chunk.m3u8", rewritten); } [Fact] public void Absolute_resolves_relative_and_keeps_absolute() { Assert.Equal("https://cdn.example/a/b.ts", HlsRewriter.Absolute(new Uri("https://cdn.example/a/p.m3u8?x=1"), "b.ts")); Assert.Equal("https://cdn.example/b.ts", HlsRewriter.Absolute(new Uri("https://cdn.example/a/p.m3u8"), "/b.ts")); Assert.Equal("https://other/b.ts", HlsRewriter.Absolute(new Uri("https://cdn.example/a/p.m3u8"), "https://other/b.ts")); } }