refactor(uaflix): handle multiple iframe sources on episode pages

Extract iframe URLs from video-box containers, fallback to all page
iframes, and include og:video:iframe metadata so pages with multiple
players are parsed consistently. Update the episode resolver to probe
all zetvideo embeds and aggregate playable streams instead of relying
on a single iframe. Bump the module version to 5.3.
This commit is contained in:
Felix 2026-05-29 16:25:17 +03:00
parent d2c7ee7c72
commit dd40ed69f2
2 changed files with 125 additions and 33 deletions

View File

@ -19,7 +19,7 @@ namespace LME.Uaflix
{ {
public class ModInit : IModuleLoaded public class ModInit : IModuleLoaded
{ {
public static double Version => 5.2; public static double Version => 5.3;
public static UaflixSettings UaFlix; public static UaflixSettings UaFlix;

View File

@ -241,6 +241,51 @@ namespace LME.Uaflix
return ExtractIframeFromMeta(doc); return ExtractIframeFromMeta(doc);
} }
/// <summary>
/// Отримати всі iframe URL зі сторінки (з video-box, загальних iframe та og:meta)
/// Використовується для сторінок з кількома плеєрами (напр. zetvideo з субтитрами)
/// </summary>
private List<string> ExtractAllIframeUrls(HtmlDocument doc)
{
var result = new List<string>();
if (doc?.DocumentNode == null)
return result;
// Спочатку збираємо всі iframe з video-box контейнерів
var videoBoxNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'video-box')]//iframe");
if (videoBoxNodes != null)
{
foreach (var node in videoBoxNodes)
{
string url = NormalizeIframeUrl(node.GetAttributeValue("src", null));
if (!string.IsNullOrEmpty(url) && !result.Any(u => string.Equals(u, url, StringComparison.OrdinalIgnoreCase)))
result.Add(url);
}
}
// Якщо нічого не знайшли в video-box, шукаємо будь-які iframe
if (result.Count == 0)
{
var allIframeNodes = doc.DocumentNode.SelectNodes("//iframe");
if (allIframeNodes != null)
{
foreach (var node in allIframeNodes)
{
string url = NormalizeIframeUrl(node.GetAttributeValue("src", null));
if (!string.IsNullOrEmpty(url) && !result.Any(u => string.Equals(u, url, StringComparison.OrdinalIgnoreCase)))
result.Add(url);
}
}
}
// Також додаємо URL з og:video:iframe meta
string metaIframe = ExtractIframeFromMeta(doc);
if (!string.IsNullOrEmpty(metaIframe) && !result.Any(u => string.Equals(u, metaIframe, StringComparison.OrdinalIgnoreCase)))
result.Add(metaIframe);
return result;
}
private async Task<(string iframeUrl, string playerType)> ProbeEpisodePlayer(string pageUrl) private async Task<(string iframeUrl, string playerType)> ProbeEpisodePlayer(string pageUrl)
{ {
if (string.IsNullOrWhiteSpace(pageUrl)) if (string.IsNullOrWhiteSpace(pageUrl))
@ -1896,6 +1941,42 @@ namespace LME.Uaflix
} }
} }
// Отримуємо всі iframe зі сторінки (підтримка кількох плеєрів, напр. zetvideo з субтитрами)
var allIframes = ExtractAllIframeUrls(doc);
// Фільтруємо zetvideo iframe — повертаємо всі як окремі потоки
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)
{
// Перший потік → "Uaflix" (переклад), наступні → "Оригінал"
string label = streamIndex == 0 ? "Uaflix" : "Оригінал";
stream.title = label;
_onLog($"ParseEpisode: zetvideo потік #{streamIndex + 1}: {label} -> {zetIframe}" +
(stream.subtitles != null && stream.subtitles.Value.data != null && stream.subtitles.Value.data.Count > 0
? " (має субтитри)" : ""));
}
result.streams.AddRange(streams);
streamIndex++;
}
}
else
{
// Старий код: використовуємо перший iframe для ashdi/інших
string iframeUrl = ExtractIframeUrl(doc); string iframeUrl = ExtractIframeUrl(doc);
if (!string.IsNullOrEmpty(iframeUrl)) if (!string.IsNullOrEmpty(iframeUrl))
{ {
@ -1912,9 +1993,7 @@ namespace LME.Uaflix
return result; return result;
} }
if (iframeUrl.Contains("zetvideo.net")) if (iframeUrl.Contains("ashdi.vip"))
result.streams = await ParseAllZetvideoSources(iframeUrl);
else if (iframeUrl.Contains("ashdi.vip"))
{ {
// Перевіряємо, чи це ashdi-vod (окремий епізод) або ashdi-serial (багатосерійний плеєр) // Перевіряємо, чи це ashdi-vod (окремий епізод) або ashdi-serial (багатосерійний плеєр)
if (iframeUrl.Contains("/vod/")) if (iframeUrl.Contains("/vod/"))
@ -1936,6 +2015,7 @@ namespace LME.Uaflix
} }
} }
} }
}
catch (Exception ex) catch (Exception ex)
{ {
_onLog($"ParseEpisode error: {ex.Message}"); _onLog($"ParseEpisode error: {ex.Message}");
@ -1988,15 +2068,27 @@ namespace LME.Uaflix
var script = doc.DocumentNode.SelectSingleNode("//script[contains(text(), 'file:')]"); var script = doc.DocumentNode.SelectSingleNode("//script[contains(text(), 'file:')]");
if (script != null) if (script != null)
{ {
var match = Regex.Match(script.InnerText, @"file:\s*""([^""]+\.m3u8)"); var fileMatch = Regex.Match(script.InnerText, @"file:\s*""([^""]+\.m3u8)");
if (match.Success) if (fileMatch.Success)
{ {
string link = match.Groups[1].Value; string link = fileMatch.Groups[1].Value;
// Парсимо subtitle з того ж Playerjs конфігу
SubtitleTpl? subtitles = null;
var subtitleMatch = Regex.Match(script.InnerText, @"subtitle:\s*""([^""]*)""");
if (subtitleMatch.Success)
{
string subtitleStr = subtitleMatch.Groups[1].Value;
if (!string.IsNullOrWhiteSpace(subtitleStr))
subtitles = ApnHelper.ParseSubtitles(subtitleStr);
}
result.Add(new PlayStream result.Add(new PlayStream
{ {
link = link, link = link,
quality = "1080p", quality = "1080p",
title = BuildDisplayTitle("Основне джерело", link, 1) title = BuildDisplayTitle("Основне джерело", link, 1),
subtitles = subtitles
}); });
return result; return result;
} }