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.
This commit is contained in:
Felix 2026-05-29 18:08:27 +03:00
parent 444e4c876e
commit 6a398317a4
2 changed files with 166 additions and 3 deletions

View File

@ -14,6 +14,7 @@ using System.Text;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Shared.Models; using Shared.Models;
using LME.Uaflix.Models; using LME.Uaflix.Models;
using LME.Uaflix;
namespace LME.Uaflix.Controllers namespace LME.Uaflix.Controllers
{ {
@ -107,7 +108,7 @@ namespace LME.Uaflix.Controllers
} }
OnLog("=== RETURN: play no streams ==="); 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') // Якщо є episode_url але немає play=true, це виклик для отримання інформації про стрім (для method: 'call')
@ -145,7 +146,7 @@ namespace LME.Uaflix.Controllers
} }
OnLog("=== RETURN: call method no streams ==="); 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; string filmUrl = href;
@ -370,9 +371,37 @@ namespace LME.Uaflix.Controllers
} }
else // Фільм 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 (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 ==="); OnLog("=== RETURN: movie no streams ===");
return OnError("lme_uaflix", refresh_proxy: true); return OnError("lme_uaflix", refresh_proxy: true);
} }

View File

@ -18,6 +18,13 @@ using System.Text;
namespace LME.Uaflix 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 public class UaflixInvoke
{ {
private static readonly Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase); 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; return result;
} }
/// <summary>
/// Parse episode with page status detection — distinguishes between
/// "page not found", "page exists but no player", and "has streams"
/// </summary>
public async Task<(PlayResult result, PageStatus status)> ParseEpisodeWithStatus(string url)
{
var result = new PlayResult() { streams = new List<PlayStream>() };
try
{
var headers = new List<HeadersModel>()
{
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) private void NormalizeUaflixVoiceNames(SerialAggregatedStructure structure)
{ {
const string baseName = "Uaflix"; const string baseName = "Uaflix";