From 50b39fa40bdbc2d4d2bb0fab9c252b13b42028b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kacprzak?= Date: Fri, 23 Oct 2026 18:34:23 +0200 Subject: [PATCH] Bound bouquet lookup and preserve playback if OpenWebif is unavailable --- Jellyfin.Plugin.Enigma2/LiveTvService.cs | 20 ++++++++++++++++++-- README.md | 4 ++-- Tests/SavedChannelPlaybackTests.cs | 23 ++++++++++++++++++++++- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Plugin.Enigma2/LiveTvService.cs b/Jellyfin.Plugin.Enigma2/LiveTvService.cs index 04f4130..93c0f41 100644 --- a/Jellyfin.Plugin.Enigma2/LiveTvService.cs +++ b/Jellyfin.Plugin.Enigma2/LiveTvService.cs @@ -1145,9 +1145,25 @@ namespace Jellyfin.Plugin.Enigma2 _logger.LogInformation("[Enigma2] Start GetChannelStream"); var requestedReference = channelOid; - var currentChannels = await GetChannelsAsync(cancellationToken).ConfigureAwait(false); + IEnumerable currentChannels = Array.Empty(); + using (var lookupDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) + { + lookupDeadline.CancelAfter(TimeSpan.FromSeconds(5)); + try + { + currentChannels = await GetChannelsAsync(lookupDeadline.Token).ConfigureAwait(false); + } + catch (Exception error) when (!cancellationToken.IsCancellationRequested + && (error is OperationCanceledException || error is HttpRequestException + || error is IOException || error is ApplicationException)) + { + // A stale-reference repair must not prevent direct streaming + // when the receiver's web interface is temporarily unavailable. + _logger.LogWarning("[Enigma2] Current bouquet lookup unavailable ({FailureType}); retaining requested channel reference", error.GetType().Name); + } + } cancellationToken.ThrowIfCancellationRequested(); - channelOid = ChannelReferenceResolver.Resolve(channelOid, currentChannels, () => + channelOid = !currentChannels.Any() ? channelOid : ChannelReferenceResolver.Resolve(channelOid, currentChannels, () => { // Native playback has already checked the user's channel access. // This internal lookup reads only Enigma2 channel metadata and diff --git a/README.md b/README.md index 7660df6..b246bb8 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ See [integration changes and deployment](README.integration.md). Internet EPG ar ## Kandydat 7.0.9 — stare wpisy kanałów po migracji, 23.10.2026 -Odczyt metadanych LiveTvChannel potwierdził stare i nowe wpisy Pulsu/Pulsu 2 oraz stare wpisy WP i TVN24 po zmianie bukietów dekodera. GetChannelStream odświeża listę z dekodera i rozwiązuje zapamiętaną referencję przed zapem, rejestracją relay i wyznaczeniem SID do probe. Najpierw zachowuje dokładny aktualny wpis; potem dopuszcza jednoznaczną zmianę samego typu usługi DVB; migrację SID/TSID rozpoznaje tylko po unikalnej dokładnej nazwie odczytanej z historycznego kanału Enigma2. Brak dopasowania lub niejednoznaczność zachowuje poprzednie zachowanie. Nie zgadujemy HD/SD ani wariantów regionalnych. Natywny identyfikator kanału i preferencje użytkownika pozostają zachowane. Stary i nowy wpis wskazują ten sam adres relay, więc nie otwierają dwóch źródeł. +Odczyt metadanych LiveTvChannel potwierdził stare i nowe wpisy Pulsu/Pulsu 2 oraz stare wpisy WP i TVN24 po zmianie bukietów dekodera. GetChannelStream odświeża listę z dekodera i rozwiązuje zapamiętaną referencję przed zapem, rejestracją relay i wyznaczeniem SID do probe. Najpierw zachowuje dokładny aktualny wpis; potem dopuszcza jednoznaczną zmianę samego typu usługi DVB; migrację SID/TSID rozpoznaje tylko po unikalnej dokładnej nazwie odczytanej z historycznego kanału Enigma2. Brak dopasowania lub niejednoznaczność zachowuje poprzednie zachowanie. Nie zgadujemy HD/SD ani wariantów regionalnych. Natywny identyfikator kanału i preferencje użytkownika pozostają zachowane. Stary i nowy wpis wskazują ten sam adres relay, więc nie otwierają dwóch źródeł. Odczyt bukietu ma wspólny limit 5 s; niedostępny lub zawieszony OpenWebif zachowuje próbę odtwarzania oryginalnej referencji. Anulowanie przez klienta pozostaje obowiązujące. -100 kontroli .NET10 przeszło, w tym rzeczywiste GetChannelStream z atrapą OpenWebif i natywnej biblioteki: nowy SID 5105, wspólne metadane i jedno połączenie relay dla starego/nowego Pulsu, anulowanie bez dodatkowego połączenia. Build Release: 0 ostrzeżeń, 0 błędów. Restore korzystał z cache pakietów bez sieci; NuGetAudit wyłączono jedynie dla tego odtworzenia offline. Nie potwierdza to odbioru na zalogowanym urządzeniu. +102 kontrole .NET10 przeszły, w tym rzeczywiste GetChannelStream z atrapą OpenWebif i natywnej biblioteki: nowy SID 5105, wspólne metadane i jedno połączenie relay dla starego/nowego Pulsu, anulowanie bez dodatkowego połączenia. Build Release: 0 ostrzeżeń, 0 błędów. Restore korzystał z cache pakietów bez sieci; NuGetAudit wyłączono jedynie dla tego odtworzenia offline. Nie potwierdza to odbioru na zalogowanym urządzeniu. Status na moment zapisu: **7.0.9 przygotowana, niewdrożona**. W produkcji pozostaje 7.0.8. Nowa sesja Wydarzeń 24 rozpoczęta o 16:21:45 UTC dała poprawne serwerowe HLS (248 klatek 1080p / 9,88 s oraz 10,027 s audio). Restart wymagany do podmiany DLL czeka na zgodę dotyczącą tej aktywnej sesji. Osobny brak odkodowanego wyjścia CANAL+4K wymaga diagnostyki źródła ECM i nie jest naprawiany przez ten resolver. Nie zmieniono ustawień Jellyfina, wrapperów, plików Web ani konfiguracji dekodera. diff --git a/Tests/SavedChannelPlaybackTests.cs b/Tests/SavedChannelPlaybackTests.cs index 5112955..3641a7f 100644 --- a/Tests/SavedChannelPlaybackTests.cs +++ b/Tests/SavedChannelPlaybackTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; @@ -54,6 +55,14 @@ public static class SavedChannelPlaybackTests 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"); } + + var offline=new LiveTvService(NullLogger.Instance,new ReceiverFactory(new UnavailableBouquet(false)),null,probe,relay,library); + check((await offline.GetChannelStream(Current,null,CancellationToken.None)).Path==current.Path&&((SavedChannelLibrary)(object)library).Calls==1, + "unavailable web interface preserves direct playback without historical lookup"); + var stalled=new LiveTvService(NullLogger.Instance,new ReceiverFactory(new UnavailableBouquet(true)),null,probe,relay,library); + var watch=Stopwatch.StartNew(); + check((await stalled.GetChannelStream(Current,null,CancellationToken.None)).Path==current.Path&&watch.Elapsednew(new BouquetHandler()); + private readonly HttpMessageHandler handler; + public ReceiverFactory(HttpMessageHandler handler=null){this.handler=handler;} + public HttpClient CreateClient(string name)=>new(handler??new BouquetHandler()); + } + private sealed class UnavailableBouquet:HttpMessageHandler + { + private readonly bool stall; + public UnavailableBouquet(bool stall){this.stall=stall;} + protected override async Task SendAsync(HttpRequestMessage request,CancellationToken token) + { + if(stall)await Task.Delay(Timeout.InfiniteTimeSpan,token); + throw new HttpRequestException("Receiver web interface unavailable"); + } } private sealed class BouquetHandler:HttpMessageHandler {