using System; using System.Linq; using System.Text.Json.Nodes; using System.IO; using MediaBrowser.Common.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; namespace Jellyfin.Plugin.Enigma2; /// Apply presentation preferences after the native API has enforced access rights. public sealed class NativeChannelFilter : IStartupFilter { public Action Configure(Action next) => app => { app.Use(async (context, continuePipeline) => { if (!HttpMethods.IsGet(context.Request.Method) || !context.Request.Path.Value.TrimEnd('/').Equals("/LiveTv/Channels",StringComparison.OrdinalIgnoreCase)) { await continuePipeline(); return; } var originalQuery=context.Request.QueryString; var originalEncoding=context.Request.Headers.AcceptEncoding; // Buffer JSON before applying preferences; compression must not run inside this buffer. context.Request.Headers.Remove("Accept-Encoding"); var parsed=QueryHelpers.ParseQuery(originalQuery.Value ?? ""); int Number(string name,int fallback) => parsed.TryGetValue(name,out var value) && int.TryParse(value,out var number) && number>=0 ? number : fallback; var start=Number("StartIndex",0);var limit=Number("Limit",int.MaxValue); // Fetch the permission-filtered full result, then paginate after preferences. context.Request.QueryString=QueryString.Create(parsed.Where(p=>!p.Key.Equals("StartIndex",StringComparison.OrdinalIgnoreCase)&&!p.Key.Equals("Limit",StringComparison.OrdinalIgnoreCase))); var originalBody=context.Response.Body; await using var buffer=new MemoryStream();context.Response.Body=buffer; try { await continuePipeline(); context.Response.Body=originalBody; if (context.Response.StatusCode==200 && context.Response.ContentType?.StartsWith("application/json",StringComparison.OrdinalIgnoreCase)==true) { buffer.Position=0; var document=await JsonNode.ParseAsync(buffer,cancellationToken:context.RequestAborted); if(document?["Items"] is JsonArray items) { Guid.TryParse(context.User.FindFirst("Jellyfin-UserId")?.Value,out var userId); if(userId==Guid.Empty && parsed.TryGetValue("UserId",out var requested)&&Guid.TryParse(requested,out var requestedId)) userId=requestedId; var preferences=userId==Guid.Empty?new ChannelPreferences():await context.RequestServices.GetRequiredService().Load(userId,context.RequestAborted); var hidden=preferences.HiddenIds.ToHashSet(StringComparer.OrdinalIgnoreCase); var includeHidden=parsed.TryGetValue("DecoderIncludeHidden",out var includeValue)&&bool.TryParse(includeValue,out var include)&&include; var order=preferences.OrderedIds.Select((id,index)=>(id,index)).ToDictionary(x=>x.id,x=>x.index,StringComparer.OrdinalIgnoreCase); string Id(JsonNode node) => Guid.TryParse((string)node?["Id"],out var id)?id.ToString("N"):""; var filtered=items.Where(item=>includeHidden||!hidden.Contains(Id(item))).OrderBy(item=>order.TryGetValue(Id(item),out var position)?position:int.MaxValue).ToArray(); document["TotalRecordCount"]=filtered.Length; document["StartIndex"]=start; document["Items"]=new JsonArray(filtered.Skip(start).Take(limit).Select(item=>item.DeepClone()).ToArray()); context.Response.ContentLength=null; await context.Response.WriteAsync(document.ToJsonString(),context.RequestAborted); return; } } buffer.Position=0;await buffer.CopyToAsync(originalBody,context.RequestAborted); } finally { context.Response.Body=originalBody;context.Request.QueryString=originalQuery;context.Request.Headers.AcceptEncoding=originalEncoding; } }); next(app); }; }