lampac-ukraine/LME.UAKino/Controller.cs
Felix 80f0869401 fix(uakino): add HTML page fallback when playlist API returns no data
Handle cases where UAKino playlist requests return empty results by
resolving stream URLs directly from the content page HTML.

Add a fallback parser that extracts video sources from `link[itemprop=video]`
or `iframe#pre`, and use it to return playable movie or single-episode
responses instead of failing immediately.

Ensure Ashdi links consistently include `multivoice` for movie playback,
including episode file URLs, to improve stream compatibility.
2026-05-15 19:06:29 +03:00

307 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using LME.UAKino.Models;
using Microsoft.AspNetCore.Mvc;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.Settings;
using Shared.Models.Templates;
namespace LME.UAKino.Controllers
{
public class Controller : BaseOnlineController
{
ProxyManager proxyManager;
public Controller() : base(ModInit.Settings)
{
proxyManager = new ProxyManager(ModInit.UAKino);
}
[HttpGet]
[Route("lite/lme_uakino")]
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, bool rjson = false, string href = null, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = loadKit(ModInit.UAKino);
if (!init.enable)
return Forbid();
var invoke = new UAKinoInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch)
{
if (!IsCheckOnlineSearchEnabled())
return OnError("lme_uakino", refresh_proxy: true);
var searchResults = await invoke.Search(title, original_title, year, imdb_id);
if (searchResults != null && searchResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8");
return OnError("lme_uakino", refresh_proxy: true);
}
string newsId = null;
string itemUrl = href;
if (string.IsNullOrEmpty(itemUrl))
{
// === ПЕРШИЙ ЗАПИТ: пошук ===
var searchResults = await invoke.Search(title, original_title, year, imdb_id);
if (searchResults == null || searchResults.Count == 0)
return OnError("lme_uakino", refresh_proxy: true);
if (serial == 1)
{
// Серіал
if (searchResults.Count == 1)
{
var sr = searchResults[0];
if (sr.Seasons.Count > 1 && s == -1)
{
// Кілька сезонів — показуємо SeasonTpl для вибору
return HandleSeasonSelection(sr, id, imdb_id, kinopoisk_id, title, original_title, year, rjson);
}
// Один сезон — використовуємо його
itemUrl = sr.Seasons[0].Url;
newsId = sr.Seasons[0].NewsId;
}
else
{
// Кілька різних шоу — обирає
return ShowSimilarTpl(searchResults, id, imdb_id, kinopoisk_id, title, original_title, year, serial, rjson);
}
}
else
{
// Фільм
if (searchResults.Count > 1)
{
return ShowSimilarTpl(searchResults, id, imdb_id, kinopoisk_id, title, original_title, year, serial, rjson);
}
itemUrl = searchResults[0].Seasons[0].Url;
newsId = searchResults[0].Seasons[0].NewsId;
}
}
else
{
// Повторний запит (з селектора сезонів або озвучок)
newsId = UAKinoInvoke.ExtractNewsId(itemUrl);
}
if (string.IsNullOrEmpty(newsId))
return OnError("lme_uakino", refresh_proxy: true);
var voices = await invoke.GetPlaylist(newsId);
if (voices == null || voices.Count == 0)
{
// Fallback: playlist API повернув ERR_NOT_DATA — пробуємо зі сторінки
string fallbackUrl = await invoke.GetPageFallbackUrl(itemUrl);
if (!string.IsNullOrEmpty(fallbackUrl))
{
if (serial == 1)
{
var voice_tpl = new VoiceTpl();
var episode_tpl = new EpisodeTpl();
string streamUrl = BuildStreamUrl(init, fallbackUrl);
voice_tpl.Append("Озвучення", true, null);
episode_tpl.Append("Епізод 1", title ?? original_title, s >= 0 ? s.ToString() : "1", "01", streamUrl);
episode_tpl.Append(voice_tpl);
return rjson
? Content(episode_tpl.ToJson(), "application/json; charset=utf-8")
: Content(episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
else
{
if (ApnHelper.IsAshdiUrl(fallbackUrl) && !fallbackUrl.Contains("multivoice"))
fallbackUrl += (fallbackUrl.Contains("?") ? "&" : "?") + "multivoice";
string streamUrl = BuildStreamUrl(init, fallbackUrl);
var movie_tpl = new MovieTpl(title, original_title);
movie_tpl.Append("Фільм", streamUrl);
return rjson
? Content(movie_tpl.ToJson(), "application/json; charset=utf-8")
: Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
}
}
return OnError("lme_uakino", refresh_proxy: true);
}
if (serial == 1)
{
return HandleSerial(init, voices, title, original_title, imdb_id, kinopoisk_id, itemUrl, s, t, rjson);
}
else
{
return HandleMovie(init, voices, title, original_title, rjson);
}
}
/// <summary>Вибір сезону для багатосезонного серіалу</summary>
private ActionResult HandleSeasonSelection(SearchResult sr, long id, string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool rjson)
{
var season_tpl = new SeasonTpl(sr.Seasons.Count);
foreach (var season in sr.Seasons)
{
string link = $"{host}/lite/lme_uakino?id={id}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season.SeasonNumber}&href={HttpUtility.UrlEncode(season.Url)}";
season_tpl.Append($"Сезон {season.SeasonNumber}", link, season.SeasonNumber.ToString());
}
return rjson
? Content(season_tpl.ToJson(), "application/json; charset=utf-8")
: Content(season_tpl.ToHtml(), "text/html; charset=utf-8");
}
/// <summary>Вибір між різними шоу/фільмами</summary>
private ActionResult ShowSimilarTpl(List<SearchResult> searchResults, long id, string imdb_id, long kinopoisk_id, string title, string original_title, int year, int serial, bool rjson)
{
var similar_tpl = new SimilarTpl(searchResults.Count);
foreach (var res in searchResults)
{
string seasonUrl = res.Seasons.Count > 0 ? res.Seasons[0].Url : "";
string yearStr = res.Seasons.Count > 0 ? (res.Seasons[0].Year?.ToString() ?? "") : (res.Year?.ToString() ?? "");
string link = $"{host}/lite/lme_uakino?id={id}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(seasonUrl)}";
similar_tpl.Append(res.Title, yearStr, res.OriginalTitle ?? "", link, res.Poster);
}
return rjson
? Content(similar_tpl.ToJson(), "application/json; charset=utf-8")
: Content(similar_tpl.ToHtml(), "text/html; charset=utf-8");
}
/// <summary>Серіал: озвучки + епізоди</summary>
private ActionResult HandleSerial(OnlinesSettings init, List<VoiceGroup> voices, string title, string original_title, string imdb_id, long kinopoisk_id, string itemUrl, int s, string t, bool rjson)
{
var voice_tpl = new VoiceTpl();
var episode_tpl = new EpisodeTpl();
if (string.IsNullOrEmpty(t))
t = voices.First().DataId;
foreach (var voice in voices)
{
string voiceLink = $"{host}/lite/lme_uakino?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&serial=1&s={s}&t={voice.DataId}&href={HttpUtility.UrlEncode(itemUrl)}";
voice_tpl.Append(voice.Name, voice.DataId == t, voiceLink);
}
var selected = voices.FirstOrDefault(v => v.DataId == t);
if (selected == null || selected.Episodes.Count == 0)
return OnError("lme_uakino", refresh_proxy: true);
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(voice_tpl);
return rjson
? Content(episode_tpl.ToJson(), "application/json; charset=utf-8")
: Content(episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
/// <summary>Фільм: список стрімів</summary>
private ActionResult HandleMovie(OnlinesSettings init, List<VoiceGroup> voices, string title, string original_title, bool rjson)
{
var movie_tpl = new MovieTpl(title, original_title);
foreach (var voice in voices)
{
foreach (var ep in voice.Episodes)
{
string label = voice.Name;
if (voices.Count == 1 && voice.Episodes.Count > 1)
label = ep.Title;
string fileUrl = ep.FileUrl;
if (ApnHelper.IsAshdiUrl(fileUrl) && !fileUrl.Contains("multivoice"))
fileUrl += (fileUrl.Contains("?") ? "&" : "?") + "multivoice";
string streamUrl = BuildStreamUrl(init, fileUrl);
movie_tpl.Append(label, streamUrl);
}
}
return rjson
? Content(movie_tpl.ToJson(), "application/json; charset=utf-8")
: Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
}
string BuildStreamUrl(OnlinesSettings init, string streamLink)
{
string link = StripLampacArgs(streamLink?.Trim());
if (string.IsNullOrEmpty(link))
return link;
if (ApnHelper.IsEnabled(init))
{
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
return ApnHelper.WrapUrl(init, link);
var noApn = (OnlinesSettings)init.Clone();
noApn.apnstream = false;
noApn.apn = null;
return HostStreamProxy(noApn, link);
}
return HostStreamProxy(init, link);
}
private static string StripLampacArgs(string url)
{
if (string.IsNullOrEmpty(url))
return url;
string cleaned = System.Text.RegularExpressions.Regex.Replace(
url,
@"([?&])(account_email|uid|nws_id)=[^&]*",
"$1",
System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned;
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}