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.
This commit is contained in:
Felix 2026-05-15 19:26:55 +03:00
parent 80f0869401
commit 15ff1ee10c
2 changed files with 78 additions and 9 deletions

View File

@ -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
}
/// <summary>Фільм: список стрімів</summary>
private ActionResult HandleMovie(OnlinesSettings init, List<VoiceGroup> voices, string title, string original_title, bool rjson)
private async Task<ActionResult> HandleMovie(OnlinesSettings init, List<VoiceGroup> voices, string title, string original_title, bool rjson, UAKinoInvoke invoke)
{
var processed = new HashSet<string>();
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);
}
}

View File

@ -193,6 +193,71 @@ namespace LME.UAKino
}
}
/// <summary>
/// Резолв Ashdi VOD сторінки: отримати реальний .m3u8 стрім з Playerjs file:'...'
/// </summary>
public async Task<string> 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<HeadersModel>()
{
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;
}
}
/// <summary>
/// Витягнути news_id з URL контенту
/// </summary>