From 15ff1ee10c28e9041b3e06466c47aee8ad818ec6 Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 15 May 2026 19:26:55 +0300 Subject: [PATCH] refactor(uakino): centralize Ashdi VOD resolution and dedupe movie streams Move Ashdi URL handling into a dedicated resolver in `UAKinoInvoke` and reuse it from controller paths instead of appending query params inline. Resolve Ashdi pages to direct player file URLs, with fallback extraction strategies and safe error handling, then build stream links from resolved targets. Avoid duplicate movie entries by skipping already processed resolved URLs, and propagate the selected season number when rendering episode metadata. --- LME.UAKino/Controller.cs | 22 +++++++------ LME.UAKino/UAKinoInvoke.cs | 65 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/LME.UAKino/Controller.cs b/LME.UAKino/Controller.cs index 41b9ed7..ef65682 100644 --- a/LME.UAKino/Controller.cs +++ b/LME.UAKino/Controller.cs @@ -118,9 +118,8 @@ namespace LME.UAKino.Controllers } else { - if (ApnHelper.IsAshdiUrl(fallbackUrl) && !fallbackUrl.Contains("multivoice")) - fallbackUrl += (fallbackUrl.Contains("?") ? "&" : "?") + "multivoice"; - string streamUrl = BuildStreamUrl(init, fallbackUrl); + string resolvedUrl = await invoke.ResolveAshdiVod(fallbackUrl); + string streamUrl = BuildStreamUrl(init, resolvedUrl); var movie_tpl = new MovieTpl(title, original_title); movie_tpl.Append("Фільм", streamUrl); return rjson @@ -137,7 +136,7 @@ namespace LME.UAKino.Controllers } else { - return HandleMovie(init, voices, title, original_title, rjson); + return await HandleMovie(init, voices, title, original_title, rjson, invoke); } } @@ -192,12 +191,13 @@ namespace LME.UAKino.Controllers if (selected == null || selected.Episodes.Count == 0) return OnError("lme_uakino", refresh_proxy: true); + string seasonStr = s >= 0 ? s.ToString() : "1"; foreach (var ep in selected.Episodes.OrderBy(e => e.EpisodeNumber ?? int.MaxValue)) { int epNum = ep.EpisodeNumber ?? 1; string epName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {epNum}" : ep.Title; string streamUrl = BuildStreamUrl(init, ep.FileUrl); - episode_tpl.Append(epName, title ?? original_title, "1", epNum.ToString("D2"), streamUrl); + episode_tpl.Append(epName, title ?? original_title, seasonStr, epNum.ToString("D2"), streamUrl); } episode_tpl.Append(voice_tpl); @@ -208,8 +208,9 @@ namespace LME.UAKino.Controllers } /// Фільм: список стрімів - private ActionResult HandleMovie(OnlinesSettings init, List voices, string title, string original_title, bool rjson) + private async Task HandleMovie(OnlinesSettings init, List voices, string title, string original_title, bool rjson, UAKinoInvoke invoke) { + var processed = new HashSet(); var movie_tpl = new MovieTpl(title, original_title); foreach (var voice in voices) @@ -221,10 +222,13 @@ namespace LME.UAKino.Controllers label = ep.Title; string fileUrl = ep.FileUrl; - if (ApnHelper.IsAshdiUrl(fileUrl) && !fileUrl.Contains("multivoice")) - fileUrl += (fileUrl.Contains("?") ? "&" : "?") + "multivoice"; + // Резолвимо Ashdi VOD — отримуємо реальний .m3u8 стрім + string resolvedUrl = await invoke.ResolveAshdiVod(fileUrl); + // Дедуплікація: якщо той самий стрім — пропускаємо + if (!processed.Add(resolvedUrl)) + continue; - string streamUrl = BuildStreamUrl(init, fileUrl); + string streamUrl = BuildStreamUrl(init, resolvedUrl); movie_tpl.Append(label, streamUrl); } } diff --git a/LME.UAKino/UAKinoInvoke.cs b/LME.UAKino/UAKinoInvoke.cs index e8cb8da..22a553e 100644 --- a/LME.UAKino/UAKinoInvoke.cs +++ b/LME.UAKino/UAKinoInvoke.cs @@ -193,6 +193,71 @@ namespace LME.UAKino } } + /// + /// Резолв Ashdi VOD сторінки: отримати реальний .m3u8 стрім з Playerjs file:'...' + /// + public async Task ResolveAshdiVod(string vodUrl) + { + if (string.IsNullOrEmpty(vodUrl) || !ApnHelper.IsAshdiUrl(vodUrl)) + return vodUrl; + + try + { + string fetchUrl = vodUrl; + if (!fetchUrl.Contains("multivoice")) + fetchUrl += (fetchUrl.Contains("?") ? "&" : "?") + "multivoice"; + + _onLog?.Invoke($"UAKino resolve Ashdi: {fetchUrl}"); + + var headers = new List() + { + new HeadersModel("User-Agent", Http.UserAgent), + new HeadersModel("Referer", "https://ashdi.vip/") + }; + + if (ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost)) + fetchUrl = ApnHelper.WrapUrl(_init, fetchUrl); + + string html = await HttpGet(fetchUrl, headers); + if (string.IsNullOrEmpty(html)) + return vodUrl; + + // Спершу простий pattern file:'url' + var fileMatch = Regex.Match(html, @"file:\s*'([^']+)'", RegexOptions.IgnoreCase); + if (!fileMatch.Success) + fileMatch = Regex.Match(html, @"file:\s*""([^""]+)""", RegexOptions.IgnoreCase); + + if (fileMatch.Success) + { + string resolvedUrl = fileMatch.Groups[1].Value; + if (!string.IsNullOrEmpty(resolvedUrl) && !resolvedUrl.StartsWith("[")) + { + _onLog?.Invoke($"UAKino resolved Ashdi: {resolvedUrl}"); + return resolvedUrl; + } + } + + // Складний плеєр — пробуємо PlayerJsDecoder + try + { + var playerPayload = LME.Common.Playerjs.PlayerJsDecoder.ExtractPlayerPayload(html); + if (playerPayload?.FilePayload is string strUrl && !string.IsNullOrEmpty(strUrl)) + { + _onLog?.Invoke($"UAKino resolved Ashdi (PlayerJsDecoder): {strUrl}"); + return strUrl; + } + } + catch { } + + return vodUrl; + } + catch (Exception ex) + { + _onLog?.Invoke($"UAKino resolve Ashdi error: {ex.Message}"); + return vodUrl; + } + } + /// /// Витягнути news_id з URL контенту ///