feat(uakino): return all Ashdi VOD streams with per-track labels

Add a dedicated resolver that parses the Ashdi multivoice JSON array and
returns every available stream entry instead of a single resolved URL.

Update controller fallback handling to build MovieTpl output from the full
stream list, preserving optional item titles as display labels and defaulting
to "Фільм" when absent. This exposes separate playable variants in results
and avoids collapsing multivoice entries into one stream link.
This commit is contained in:
Felix 2026-05-15 20:03:34 +03:00
parent 460f527a6f
commit b6a884d6e3
2 changed files with 80 additions and 4 deletions

View File

@ -118,10 +118,14 @@ namespace LME.UAKino.Controllers
}
else
{
string resolvedUrl = await invoke.ResolveAshdiVod(fallbackUrl);
string streamUrl = BuildStreamUrl(init, resolvedUrl);
var movie_tpl = new MovieTpl(title, original_title);
movie_tpl.Append("Фільм", streamUrl);
var resolvedStreams = await invoke.ResolveAshdiVodAll(fallbackUrl);
var movie_tpl = new MovieTpl(title, original_title, resolvedStreams.Count);
foreach (var (file, label) in resolvedStreams)
{
string displayLabel = !string.IsNullOrEmpty(label) ? label : "Фільм";
string streamUrl = BuildStreamUrl(init, file);
movie_tpl.Append(displayLabel, streamUrl);
}
return rjson
? Content(movie_tpl.ToJson(), "application/json; charset=utf-8")
: Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");

View File

@ -270,6 +270,78 @@ namespace LME.UAKino
}
}
/// <summary>
/// Резолв Ashdi VOD з ?multivoice: повертає ВСІ стріми з JSON масиву
/// </summary>
public async Task<List<(string file, string title)>> ResolveAshdiVodAll(string vodUrl)
{
var result = new List<(string file, string title)>();
if (string.IsNullOrEmpty(vodUrl) || !ApnHelper.IsAshdiUrl(vodUrl))
{
if (!string.IsNullOrEmpty(vodUrl))
result.Add((vodUrl, null));
return result;
}
try
{
_onLog?.Invoke($"UAKino resolve Ashdi all: {vodUrl}");
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", Http.UserAgent),
new HeadersModel("Referer", "https://ashdi.vip/")
};
string fetchUrl = vodUrl;
if (ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost))
fetchUrl = ApnHelper.WrapUrl(_init, fetchUrl);
string html = await HttpGet(fetchUrl, headers);
if (string.IsNullOrEmpty(html))
{
result.Add((vodUrl, null));
return result;
}
int arrayStart = FindAshdiJsonArray(html);
if (arrayStart >= 0)
{
string jsonArray = ExtractBalancedBrackets(html, arrayStart);
if (!string.IsNullOrEmpty(jsonArray))
{
try
{
using var arr = JsonDocument.Parse(jsonArray);
if (arr.RootElement.ValueKind == JsonValueKind.Array)
{
foreach (var item in arr.RootElement.EnumerateArray())
{
string file = item.GetProperty("file").GetString();
string title = item.TryGetProperty("title", out var t) ? t.GetString() : null;
if (!string.IsNullOrEmpty(file))
result.Add((file, title?.Trim()));
}
}
}
catch { }
}
}
if (result.Count == 0)
result.Add((vodUrl, null));
return result;
}
catch (Exception ex)
{
_onLog?.Invoke($"UAKino resolve Ashdi all error: {ex.Message}");
result.Add((vodUrl, null));
return result;
}
}
/// <summary>
/// Знайти позицію JSON масиву `[{...}]` після `file:'`
/// </summary>