From 6a398317a48551a94d4c416346aa10f8a7792663 Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 29 May 2026 18:08:27 +0300 Subject: [PATCH] feat(uaflix): detect missing player and retry movie streams Add page-status detection for episode parsing so the controller can distinguish between a missing page, an existing page without a player, and a page with available streams. When a movie page exists but no player is present, retry once after a short delay before failing. If the player still is unavailable, return a non-cached error so temporary site state does not get persisted. --- LME.Uaflix/Controller.cs | 35 +++++++++- LME.Uaflix/UaflixInvoke.cs | 134 +++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 3 deletions(-) diff --git a/LME.Uaflix/Controller.cs b/LME.Uaflix/Controller.cs index 2d6cddc..1a7ddf7 100644 --- a/LME.Uaflix/Controller.cs +++ b/LME.Uaflix/Controller.cs @@ -14,6 +14,7 @@ using System.Text; using Shared.Models.Online.Settings; using Shared.Models; using LME.Uaflix.Models; +using LME.Uaflix; namespace LME.Uaflix.Controllers { @@ -107,7 +108,7 @@ namespace LME.Uaflix.Controllers } OnLog("=== RETURN: play no streams ==="); - return OnError("lme_uaflix", refresh_proxy: true); + return OnError("lme_uaflix", gbcache: false, refresh_proxy: true); } // Якщо є episode_url але немає play=true, це виклик для отримання інформації про стрім (для method: 'call') @@ -145,7 +146,7 @@ namespace LME.Uaflix.Controllers } OnLog("=== RETURN: call method no streams ==="); - return OnError("lme_uaflix", refresh_proxy: true); + return OnError("lme_uaflix", gbcache: false, refresh_proxy: true); } string filmUrl = href; @@ -370,9 +371,37 @@ namespace LME.Uaflix.Controllers } else // Фільм { - var playResult = await invoke.ParseEpisode(filmUrl); + var (playResult, pageStatus) = await invoke.ParseEpisodeWithStatus(filmUrl); + + // Retry: якщо сторінка існує, але плеєр ще не додано — чекаємо 3 сек і пробуємо знову + if (pageStatus == PageStatus.PageExistsNoPlayer + && (playResult?.streams == null || playResult.streams.Count == 0)) + { + OnLog("Movie page exists but no player found, retrying in 3 seconds..."); + await Task.Delay(3000); + + var retryResult = await invoke.ParseEpisode(filmUrl); + if (retryResult?.streams != null && retryResult.streams.Count > 0) + { + playResult = retryResult; + pageStatus = PageStatus.HasStreams; + OnLog("Retry successful: streams found after delay"); + } + else + { + OnLog("Retry: still no streams after delay"); + } + } + if (playResult?.streams == null || playResult.streams.Count == 0) { + if (pageStatus == PageStatus.PageExistsNoPlayer) + { + OnLog("=== RETURN: movie page exists but player temporarily unavailable ==="); + // gbcache: false — не кешувати помилку, щоб наступні запити не блокувалися + return OnError("Плеєр тимчасово недоступний, спробуйте пізніше", gbcache: false, refresh_proxy: false); + } + OnLog("=== RETURN: movie no streams ==="); return OnError("lme_uaflix", refresh_proxy: true); } diff --git a/LME.Uaflix/UaflixInvoke.cs b/LME.Uaflix/UaflixInvoke.cs index 4a1dd5b..f49d05f 100644 --- a/LME.Uaflix/UaflixInvoke.cs +++ b/LME.Uaflix/UaflixInvoke.cs @@ -18,6 +18,13 @@ using System.Text; namespace LME.Uaflix { + public enum PageStatus + { + PageNotFound, + PageExistsNoPlayer, // Page loaded (HTTP 200) but no playable streams found + HasStreams // At least one stream found + } + public class UaflixInvoke { private static readonly Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase); @@ -2092,6 +2099,133 @@ namespace LME.Uaflix return result; } + /// + /// Parse episode with page status detection — distinguishes between + /// "page not found", "page exists but no player", and "has streams" + /// + public async Task<(PlayResult result, PageStatus status)> ParseEpisodeWithStatus(string url) + { + var result = new PlayResult() { streams = new List() }; + try + { + var headers = new List() + { + new HeadersModel("User-Agent", "Mozilla/5.0"), + new HeadersModel("Referer", _init.host) + }; + + string html = await GetHtml(url, headers); + + if (string.IsNullOrWhiteSpace(html)) + { + _onLog($"ParseEpisodeWithStatus: Page not found or empty for {url}"); + return (result, PageStatus.PageNotFound); + } + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + var videoNode = doc.DocumentNode.SelectSingleNode("//video"); + if (videoNode != null) + { + string videoUrl = videoNode.GetAttributeValue("src", ""); + if (!string.IsNullOrEmpty(videoUrl)) + { + result.streams.Add(new PlayStream + { + link = videoUrl, + quality = "1080p", + title = BuildDisplayTitle("Основне джерело", videoUrl, 1) + }); + return (result, PageStatus.HasStreams); + } + } + + var allIframes = ExtractAllIframeUrls(doc); + + var zetvideoIframes = allIframes + .Where(u => u != null && u.Contains("zetvideo.net")) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (zetvideoIframes.Count > 0) + { + int streamIndex = 0; + foreach (var zetIframe in zetvideoIframes) + { + var streams = await ParseAllZetvideoSources(zetIframe); + if (streams == null || streams.Count == 0) + continue; + + foreach (var stream in streams) + { + string label = streamIndex == 0 ? "Uaflix" : "Оригінал"; + stream.title = label; + + _onLog($"ParseEpisodeWithStatus: zetvideo stream #{streamIndex + 1}: {label} -> {zetIframe}" + + (stream.subtitles?.data?.Count > 0 + ? " (has subtitles)" : "")); + } + + result.streams.AddRange(streams); + streamIndex++; + } + + if (result.streams.Count > 0) + return (result, PageStatus.HasStreams); + } + else + { + string iframeUrl = ExtractIframeUrl(doc); + if (!string.IsNullOrEmpty(iframeUrl)) + { + if (iframeUrl.Contains("ashdi.vip/serial/")) + { + result.ashdi_url = iframeUrl; + return (result, PageStatus.HasStreams); + } + + if (iframeUrl.Contains("youtube.com/embed/")) + { + _onLog($"ParseEpisodeWithStatus: Only YouTube trailer found on page: {iframeUrl}"); + return (result, PageStatus.PageExistsNoPlayer); + } + + if (iframeUrl.Contains("ashdi.vip")) + { + if (iframeUrl.Contains("/vod/")) + { + result.streams = await ParseAshdiVodEpisode(iframeUrl); + } + else + { + result.streams = await ParseAllAshdiSources(iframeUrl); + var idMatch = Regex.Match(iframeUrl, @"_(\d+)|vod/(\d+)"); + if (idMatch.Success) + { + string ashdiId = idMatch.Groups[1].Success ? idMatch.Groups[1].Value : idMatch.Groups[2].Value; + result.subtitles = await GetAshdiSubtitles(ashdiId); + } + } + + if (result.streams.Count > 0) + return (result, PageStatus.HasStreams); + } + } + } + + _onLog($"ParseEpisodeWithStatus: Page exists but no playable streams for {url}"); + return (result, PageStatus.PageExistsNoPlayer); + } + catch (Exception ex) + { + _onLog($"ParseEpisodeWithStatus error: {ex.Message}"); + } + + _onLog($"ParseEpisodeWithStatus result: streams.count={result.streams.Count}, ashdi_url={result.ashdi_url}"); + return (result, result.streams.Count > 0 ? PageStatus.HasStreams : PageStatus.PageNotFound); + } + private void NormalizeUaflixVoiceNames(SerialAggregatedStructure structure) { const string baseName = "Uaflix";