using System; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Plugin.Enigma2; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging.Abstractions; public static class SavedChannelPlaybackTests { public const string Old = "1:0:1:3D66:2C88:13E:820000:0:0:0:"; public const string Current = "1:0:1:13F1:5DC:13E:820000:0:0:0:"; public static async Task Run(Action check) { var previous = Plugin.Instance; var folder = Path.Combine(Path.GetTempPath(),"saved-channel-"+Guid.NewGuid().ToString("N")); Directory.CreateDirectory(folder); try { var paths=DispatchProxy.Create();((SavedPaths)(object)paths).Folder=folder; var plugin=new Plugin(paths,DispatchProxy.Create()); plugin.Configuration.HostName="receiver";plugin.Configuration.WebInterfacePort="80"; plugin.Configuration.StreamingPort="8001";plugin.Configuration.OnlyOneBouquet=true; plugin.Configuration.TVBouquet="Polskie";plugin.Configuration.FetchPiconsFromWebInterface=true; var library=DispatchProxy.Create(); var handler=new ReceiverHandler(); using var relay=new LiveStreamRelay(new HttpClient(handler),8096,TimeSpan.FromMilliseconds(50)); var calls=0; var probe=new LiveStreamProbe((url,token)=> { calls++; check(url.EndsWith("?programme=5105",StringComparison.Ordinal),"saved channel probes the migrated SID rather than the historical SID"); return Task.FromResult("{\"programs\":[{\"program_id\":5105,\"streams\":[{\"index\":0,\"codec_type\":\"video\",\"codec_name\":\"h264\",\"width\":1920,\"height\":1080}]}],\"streams\":[{\"index\":0,\"codec_type\":\"video\",\"codec_name\":\"h264\",\"width\":1920,\"height\":1080}]}"); }); var service=new LiveTvService(NullLogger.Instance,new ReceiverFactory(),null,probe,relay,library); var saved=await service.GetChannelStream(Old,null,CancellationToken.None); var current=await service.GetChannelStream(Current,null,CancellationToken.None); check(saved.Path==current.Path&&calls==1,"saved and current native channels share the same relay and metadata probe"); check(((SavedChannelLibrary)(object)library).Calls==1,"current reference avoids the native historical lookup"); var id=saved.Path.Split('/').Last(); await using var first=await relay.Subscribe(id,CancellationToken.None); await using var second=await relay.Subscribe(id,CancellationToken.None); await first.Reader.ReadAsync();await second.Reader.ReadAsync(); check(handler.Calls==1&&handler.Reference==Current,"both native IDs open only one upstream using the migrated receiver reference"); using var cancelled=new CancellationTokenSource();cancelled.Cancel(); try { await service.GetChannelStream(Old,null,cancelled.Token);throw new Exception("Expected cancellation"); } catch(OperationCanceledException) { check(handler.Calls==1,"cancelled channel lookup opens no extra upstream"); } } finally { typeof(Plugin).GetProperty(nameof(Plugin.Instance)).SetValue(null,previous); Directory.Delete(folder,true); } } public class NoXmlFiles:DispatchProxy { protected override object Invoke(MethodInfo method,object[] args) { if(method.Name=="SerializeToFile")return null; // Keep default configuration in memory. throw new Exception("No persisted configuration expected in isolated test"); } } public class SavedPaths:DispatchProxy { public string Folder; protected override object Invoke(MethodInfo method,object[] args) => method.ReturnType==typeof(string)?Folder:null; } public class SavedChannelLibrary:DispatchProxy { public int Calls; protected override object Invoke(MethodInfo method,object[] args) { if(method.Name!="GetItemList"||args[0] is not InternalItemsQuery q||!q.IncludeItemTypes.SequenceEqual(new[]{BaseItemKind.LiveTvChannel})) throw new Exception("Expected metadata lookup restricted to live TV channels"); Calls++; return new BaseItem[]{new LiveTvChannel{ExternalId=Old,Name="TV PULS HD",ServiceName="Enigma2"}, new LiveTvChannel{ExternalId=Old,Name="Different channel",ServiceName="Other provider"}}; } } private sealed class ReceiverFactory:IHttpClientFactory { public HttpClient CreateClient(string name)=>new(new BouquetHandler()); } private sealed class BouquetHandler:HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request,CancellationToken token) { token.ThrowIfCancellationRequested(); var body=string.IsNullOrEmpty(request.RequestUri.Query) ? "1:7:1:0:0:0:0:0:0:0:FROM BOUQUETPolskie" : ""+Current+"TV PULS HD"; return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK){Content=new StringContent(body)}); } } private sealed class ReceiverHandler:HttpMessageHandler { public int Calls;public string Reference; protected override Task SendAsync(HttpRequestMessage request,CancellationToken token) { Calls++;Reference=request.RequestUri.AbsolutePath.TrimStart('/'); return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK){Content=new StreamContent(new PacketStream())}); } } }