From dd40ed69f2e9cacc356bf0ad650f6e4bb2bfaefa Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 29 May 2026 16:25:17 +0300 Subject: [PATCH] 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. --- LME.Uaflix/ModInit.cs | 2 +- LME.Uaflix/UaflixInvoke.cs | 156 +++++++++++++++++++++++++++++-------- 2 files changed, 125 insertions(+), 33 deletions(-) diff --git a/LME.Uaflix/ModInit.cs b/LME.Uaflix/ModInit.cs index 92754f5..6b0280c 100644 --- a/LME.Uaflix/ModInit.cs +++ b/LME.Uaflix/ModInit.cs @@ -19,7 +19,7 @@ namespace LME.Uaflix { public class ModInit : IModuleLoaded { - public static double Version => 5.2; + public static double Version => 5.3; public static UaflixSettings UaFlix; diff --git a/LME.Uaflix/UaflixInvoke.cs b/LME.Uaflix/UaflixInvoke.cs index d50bf59..6f26567 100644 --- a/LME.Uaflix/UaflixInvoke.cs +++ b/LME.Uaflix/UaflixInvoke.cs @@ -241,6 +241,51 @@ namespace LME.Uaflix return ExtractIframeFromMeta(doc); } + /// + /// Отримати всі iframe URL зі сторінки (з video-box, загальних iframe та og:meta) + /// Використовується для сторінок з кількома плеєрами (напр. zetvideo з субтитрами) + /// + private List ExtractAllIframeUrls(HtmlDocument doc) + { + var result = new List(); + 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) { if (string.IsNullOrWhiteSpace(pageUrl)) @@ -1896,41 +1941,76 @@ namespace LME.Uaflix } } - string iframeUrl = ExtractIframeUrl(doc); - if (!string.IsNullOrEmpty(iframeUrl)) + // Отримуємо всі 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) { - if (iframeUrl.Contains("ashdi.vip/serial/")) + int streamIndex = 0; + foreach (var zetIframe in zetvideoIframes) { - result.ashdi_url = iframeUrl; - return result; - } + var streams = await ParseAllZetvideoSources(zetIframe); + if (streams == null || streams.Count == 0) + continue; - // Ігноруємо YouTube трейлери - if (iframeUrl.Contains("youtube.com/embed/")) - { - _onLog($"ParseEpisode: Ignoring YouTube trailer iframe: {iframeUrl}"); - return result; - } - - if (iframeUrl.Contains("zetvideo.net")) - result.streams = await ParseAllZetvideoSources(iframeUrl); - else if (iframeUrl.Contains("ashdi.vip")) - { - // Перевіряємо, чи це ashdi-vod (окремий епізод) або ashdi-serial (багатосерійний плеєр) - if (iframeUrl.Contains("/vod/")) + foreach (var stream in streams) { - // Це окремий епізод на ashdi.vip/vod/, обробляємо як ashdi-vod - result.streams = await ParseAshdiVodEpisode(iframeUrl); + // Перший потік → "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 + ? " (має субтитри)" : "")); } - else + + result.streams.AddRange(streams); + streamIndex++; + } + } + else + { + // Старий код: використовуємо перший iframe для ashdi/інших + string iframeUrl = ExtractIframeUrl(doc); + if (!string.IsNullOrEmpty(iframeUrl)) + { + if (iframeUrl.Contains("ashdi.vip/serial/")) { - // Це багатосерійний плеєр, обробляємо як і раніше - result.streams = await ParseAllAshdiSources(iframeUrl); - var idMatch = Regex.Match(iframeUrl, @"_(\d+)|vod/(\d+)"); - if (idMatch.Success) + result.ashdi_url = iframeUrl; + return result; + } + + // Ігноруємо YouTube трейлери + if (iframeUrl.Contains("youtube.com/embed/")) + { + _onLog($"ParseEpisode: Ignoring YouTube trailer iframe: {iframeUrl}"); + return result; + } + + if (iframeUrl.Contains("ashdi.vip")) + { + // Перевіряємо, чи це ashdi-vod (окремий епізод) або ashdi-serial (багатосерійний плеєр) + if (iframeUrl.Contains("/vod/")) { - string ashdiId = idMatch.Groups[1].Success ? idMatch.Groups[1].Value : idMatch.Groups[2].Value; - result.subtitles = await GetAshdiSubtitles(ashdiId); + // Це окремий епізод на ashdi.vip/vod/, обробляємо як ashdi-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); + } } } } @@ -1988,15 +2068,27 @@ namespace LME.Uaflix var script = doc.DocumentNode.SelectSingleNode("//script[contains(text(), 'file:')]"); if (script != null) { - var match = Regex.Match(script.InnerText, @"file:\s*""([^""]+\.m3u8)"); - if (match.Success) + var fileMatch = Regex.Match(script.InnerText, @"file:\s*""([^""]+\.m3u8)"); + 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 { link = link, quality = "1080p", - title = BuildDisplayTitle("Основне джерело", link, 1) + title = BuildDisplayTitle("Основне джерело", link, 1), + subtitles = subtitles }); return result; }