lampac-ukraine/LME.AnimeON/Controller.cs

520 lines
24 KiB
C#

using System.Text.Json;
using Shared.Engine;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Web;
using System.Linq;
using Shared;
using Shared.Models.Templates;
using LME.AnimeON.Models;
using System.Text.RegularExpressions;
using System.Text;
using Shared.Models.Online.Settings;
using Shared.Models;
using HtmlAgilityPack;
namespace LME.AnimeON.Controllers
{
public class Controller : BaseOnlineController
{
ProxyManager proxyManager;
public Controller() : base(ModInit.Settings)
{
proxyManager = new ProxyManager(ModInit.AnimeON);
}
[HttpGet]
[Route("lite/lme.animeon")]
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, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = loadKit(ModInit.AnimeON);
if (!init.enable)
return Forbid();
TryEnableMagicApn(init);
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch)
{
if (!IsCheckOnlineSearchEnabled())
return OnError("lme.animeon", refresh_proxy: true);
var checkSeasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial);
if (checkSeasons != null && checkSeasons.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8");
return OnError("lme.animeon", refresh_proxy: true);
}
OnLog($"AnimeON Index: title={title}, original_title={original_title}, serial={serial}, s={s}, t={t}, year={year}, imdb_id={imdb_id}, kp={kinopoisk_id}");
var seasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial);
OnLog($"AnimeON: search results = {seasons?.Count ?? 0}");
if (seasons == null || seasons.Count == 0)
return OnError("lme.animeon", refresh_proxy: true);
// [Refactoring] Використовується агрегована структура (AggregateSerialStructure) — попередній збір allOptions не потрібний
// [Refactoring] Перевірка allOptions видалена — використовується перевірка структури озвучок нижче
if (serial == 1)
{
if (s == -1) // Крок 1: Вибір аніме (як сезони)
{
var seasonItems = seasons
.Select((anime, index) => new
{
Anime = anime,
Index = index,
SeasonNumber = anime.Season > 0 ? anime.Season : index + 1
})
.GroupBy(x => x.SeasonNumber)
.Select(g => g.First())
.OrderBy(x => x.SeasonNumber)
.ToList();
var season_tpl = new SeasonTpl(seasonItems.Count);
foreach (var item in seasonItems)
{
string seasonName = item.SeasonNumber.ToString();
string link = $"{host}/lite/lme.animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={item.SeasonNumber}";
season_tpl.Append(seasonName, link, seasonName);
}
OnLog($"AnimeON: return seasons count={seasonItems.Count}");
return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8");
}
else // Крок 2/3: Вибір озвучки та епізодів
{
var seasonItems = seasons
.Select((anime, index) => new
{
Anime = anime,
Index = index,
SeasonNumber = anime.Season > 0 ? anime.Season : index + 1
})
.GroupBy(x => x.SeasonNumber)
.Select(g => g.First())
.OrderBy(x => x.SeasonNumber)
.ToList();
var selected = seasonItems.FirstOrDefault(x => x.SeasonNumber == s);
if (selected == null && s >= 0 && s < seasons.Count)
selected = new { Anime = seasons[s], Index = s, SeasonNumber = seasons[s].Season > 0 ? seasons[s].Season : s + 1 };
if (selected == null)
return OnError("lme.animeon", refresh_proxy: true);
var selectedAnime = selected.Anime;
int selectedSeasonNumber = selected.SeasonNumber;
var structure = await invoke.AggregateSerialStructure(selectedAnime.Id, selectedSeasonNumber);
if (structure == null || !structure.Voices.Any())
return OnError("lme.animeon", refresh_proxy: true);
OnLog($"AnimeON: voices found = {structure.Voices.Count}");
var voiceItems = structure.Voices
.Select(v =>
{
string display = v.Value?.DisplayName;
if (string.IsNullOrWhiteSpace(display))
display = v.Key;
if (string.IsNullOrWhiteSpace(display))
display = "Озвучка";
return new { Key = v.Key, Display = display };
})
.ToList();
// Автовибір першої озвучки якщо t не задано
if (string.IsNullOrEmpty(t))
t = voiceItems.First().Key;
// Формуємо список озвучок
var voice_tpl = new VoiceTpl();
foreach (var voice in voiceItems)
{
string voiceLink = $"{host}/lite/lme.animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
bool isActive = voice.Key == t;
voice_tpl.Append(voice.Display, isActive, voiceLink);
}
// Перевірка вибраної озвучки
if (!structure.Voices.ContainsKey(t))
return OnError("lme.animeon", refresh_proxy: true);
var episode_tpl = new EpisodeTpl();
var selectedVoiceInfo = structure.Voices[t];
// Формуємо епізоди для вибраної озвучки
foreach (var ep in selectedVoiceInfo.Episodes.OrderBy(e => e.Number))
{
string episodeName = !string.IsNullOrEmpty(ep.Title) ? ep.Title : $"Епізод {ep.Number}";
string seasonStr = selectedSeasonNumber.ToString();
string episodeStr = ep.Number.ToString();
// Для серіалів пріоритет — готовий fileUrl з API episodes (без додаткового резолву).
if (!string.IsNullOrEmpty(ep.Hls))
{
List<HeadersModel> streamHeaders = null;
bool forceProxy = false;
if (ep.Hls.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase))
{
streamHeaders = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://ashdi.vip/")
};
forceProxy = true;
}
string readyStreamUrl = BuildStreamUrl(init, ep.Hls, streamHeaders, forceProxy);
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, readyStreamUrl);
continue;
}
string streamLink = ep.VideoUrl;
bool needsResolve = selectedVoiceInfo.PlayerType == "moon" || selectedVoiceInfo.PlayerType == "ashdi";
if (string.IsNullOrEmpty(streamLink) && ep.EpisodeId > 0)
{
string callUrl = $"{host}/lite/lme.animeon/play?episode_id={ep.EpisodeId}&serial=1";
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
continue;
}
if (string.IsNullOrEmpty(streamLink))
continue;
if (needsResolve || streamLink.Contains("moonanime.art") || streamLink.Contains("ashdi.vip/vod"))
{
string callUrl = $"{host}/lite/lme.animeon/play?url={HttpUtility.UrlEncode(streamLink)}&serial=1";
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
}
else
{
string playUrl = BuildStreamUrl(init, streamLink, headers: null, forceProxy: false);
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, playUrl);
}
}
// Повертаємо озвучки + епізоди разом
OnLog($"AnimeON: return episodes count={selectedVoiceInfo.Episodes.Count} for voice='{t}' season={selectedAnime.Season}");
episode_tpl.Append(voice_tpl);
if (rjson)
return Content(episode_tpl.ToJson(), "application/json; charset=utf-8");
return Content(episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
}
else // Фільм
{
var firstAnime = seasons.FirstOrDefault();
if (firstAnime == null)
return OnError("lme.animeon", refresh_proxy: true);
var fundubs = await invoke.GetFundubs(firstAnime.Id);
OnLog($"AnimeON: movie fundubs count = {fundubs?.Count ?? 0}");
if (fundubs == null || fundubs.Count == 0)
return OnError("lme.animeon", refresh_proxy: true);
var tpl = new MovieTpl(title, original_title);
foreach (var fundub in fundubs)
{
if (fundub?.Fundub == null || fundub.Player == null || fundub.Player.Count == 0)
continue;
foreach (var player in fundub.Player)
{
var episodesData = await invoke.GetEpisodes(firstAnime.Id, player.Id, fundub.Fundub.Id);
if (episodesData == null || episodesData.Episodes == null || episodesData.Episodes.Count == 0)
continue;
var firstEp = episodesData.Episodes.FirstOrDefault();
if (firstEp == null)
continue;
string streamLink = !string.IsNullOrEmpty(firstEp.Hls) ? firstEp.Hls : firstEp.VideoUrl;
if (string.IsNullOrEmpty(streamLink))
continue;
string translationName = $"[{player.Name}] {fundub.Fundub.Name}";
bool needsResolve = player.Name?.ToLower() == "moon" || player.Name?.ToLower() == "ashdi";
if (streamLink.Contains("ashdi.vip/vod", StringComparison.OrdinalIgnoreCase))
{
var ashdiStreams = await invoke.ParseAshdiPageStreams(streamLink);
if (ashdiStreams != null && ashdiStreams.Count > 0)
{
foreach (var ashdiStream in ashdiStreams)
{
string optionName = $"{translationName} {ashdiStream.title}";
string callUrl = $"{host}/lite/lme.animeon/play?url={HttpUtility.UrlEncode(ashdiStream.link)}";
tpl.Append(optionName, accsArgs(callUrl), "call");
}
continue;
}
}
if (needsResolve || streamLink.Contains("moonanime.art/iframe/") || streamLink.Contains("ashdi.vip/vod"))
{
string callUrl = $"{host}/lite/lme.animeon/play?url={HttpUtility.UrlEncode(streamLink)}";
tpl.Append(translationName, accsArgs(callUrl), "call");
}
else
{
tpl.Append(translationName, BuildStreamUrl(init, streamLink, headers: null, forceProxy: false));
}
}
}
// Якщо не зібрали жодної опції — повертаємо помилку
if (tpl.data == null || tpl.data.Count == 0)
return OnError("lme.animeon", refresh_proxy: true);
OnLog("AnimeON: return movie options");
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8");
}
}
async Task<List<FundubModel>> GetFundubs(OnlinesSettings init, int animeId)
{
string fundubsUrl = $"{init.host}/api/player/{animeId}/translations";
string fundubsJson = await httpHydra.Get(fundubsUrl, newheaders: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
if (string.IsNullOrEmpty(fundubsJson))
return null;
var fundubsResponse = JsonSerializer.Deserialize<FundubsResponseModel>(fundubsJson);
if (fundubsResponse?.Translations == null || fundubsResponse.Translations.Count == 0)
return null;
var fundubs = new List<FundubModel>();
foreach (var translation in fundubsResponse.Translations)
{
var fundubModel = new FundubModel
{
Fundub = translation.Translation,
Player = translation.Player
};
fundubs.Add(fundubModel);
}
return fundubs;
}
async Task<EpisodeModel> GetEpisodes(OnlinesSettings init, int animeId, int playerId, int fundubId)
{
string episodesUrl = $"{init.host}/api/player/{animeId}/episodes?take=100&skip=-1&playerId={playerId}&translationId={fundubId}";
string episodesJson = await httpHydra.Get(episodesUrl, newheaders: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
if (string.IsNullOrEmpty(episodesJson))
return null;
return JsonSerializer.Deserialize<EpisodeModel>(episodesJson);
}
async ValueTask<List<SearchModel>> search(OnlinesSettings init, string imdb_id, long kinopoisk_id, string title, string original_title, int year)
{
string memKey = $"AnimeON:search:{kinopoisk_id}:{imdb_id}";
if (hybridCache.TryGetValue(memKey, out List<SearchModel> res))
return res;
try
{
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) };
async Task<List<SearchModel>> FindAnime(string query)
{
if (string.IsNullOrEmpty(query))
return null;
string searchUrl = $"{init.host}/api/anime/search?text={HttpUtility.UrlEncode(query)}";
string searchJson = await httpHydra.Get(searchUrl, newheaders: headers);
if (string.IsNullOrEmpty(searchJson))
return null;
var searchResponse = JsonSerializer.Deserialize<SearchResponseModel>(searchJson);
return searchResponse?.Result;
}
var searchResults = await FindAnime(title) ?? await FindAnime(original_title);
if (searchResults == null)
return null;
if (!string.IsNullOrEmpty(imdb_id))
{
var seasons = searchResults.Where(a => a.ImdbId == imdb_id).ToList();
if (seasons.Count > 0)
{
hybridCache.Set(memKey, seasons, cacheTime(5));
return seasons;
}
}
// Fallback to first result if no imdb match
var firstResult = searchResults.FirstOrDefault();
if (firstResult != null)
{
var list = new List<SearchModel> { firstResult };
hybridCache.Set(memKey, list, cacheTime(5));
return list;
}
return null;
}
catch (Exception ex)
{
OnLog($"AnimeON error: {ex.Message}");
}
return null;
}
[HttpGet("lite/lme.animeon/play")]
public async Task<ActionResult> Play(string url, int episode_id = 0, string title = null, int serial = 0)
{
await UpdateService.ConnectAsync(host);
var init = loadKit(ModInit.AnimeON);
if (!init.enable)
return Forbid();
TryEnableMagicApn(init);
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
bool disableAshdiMultivoiceForVod = serial == 1;
OnLog($"AnimeON Play: url={url}, episode_id={episode_id}, serial={serial}");
string streamLink = null;
if (episode_id > 0)
{
streamLink = await invoke.ResolveEpisodeStream(episode_id, disableAshdiMultivoiceForVod);
}
else if (!string.IsNullOrEmpty(url))
{
streamLink = await invoke.ResolveVideoUrl(url, disableAshdiMultivoiceForVod);
}
else
{
OnLog("AnimeON Play: empty url");
return OnError("lme.animeon", refresh_proxy: true);
}
if (string.IsNullOrEmpty(streamLink))
{
OnLog("AnimeON Play: cannot extract stream");
return OnError("lme.animeon", refresh_proxy: true);
}
List<HeadersModel> streamHeaders = null;
bool forceProxy = false;
if (streamLink.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase))
{
streamHeaders = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://ashdi.vip/")
};
forceProxy = true;
}
string streamUrl = BuildStreamUrl(init, streamLink, streamHeaders, forceProxy);
string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? string.Empty}\"}}";
OnLog("AnimeON Play: return call JSON");
return UpdateService.Validate(Content(jsonResult, "application/json; charset=utf-8"));
}
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;
}
string BuildStreamUrl(OnlinesSettings init, string streamLink, List<HeadersModel> headers, bool forceProxy)
{
string link = streamLink?.Trim();
if (string.IsNullOrEmpty(link))
return link;
link = StripLampacArgs(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, headers: headers, force_streamproxy: forceProxy);
}
return HostStreamProxy(init, link, headers: headers, force_streamproxy: forceProxy);
}
private void TryEnableMagicApn(OnlinesSettings init)
{
if (init == null
|| init.apn != null
|| init.streamproxy
|| string.IsNullOrWhiteSpace(ModInit.MagicApnAshdiHost))
return;
string player = new RchClient(HttpContext, host, init, requestInfo).InfoConnected()?.player;
bool useInnerPlayer = string.IsNullOrWhiteSpace(player)
|| player.Equals("inner", StringComparison.OrdinalIgnoreCase);
if (!useInnerPlayer)
return;
ApnHelper.ApplyInitConf(true, ModInit.MagicApnAshdiHost, init);
OnLog($"AnimeON: увімкнено magic_apn для Ashdi (player={player ?? "unknown"}).");
}
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);
}
}
}