using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Web; using Microsoft.AspNetCore.Mvc; using Mikai.Models; using Shared; using Shared.Engine; using Shared.Models; using Shared.Models.Online.Settings; using Shared.Models.Templates; namespace Mikai.Controllers { public class Controller : BaseOnlineController { private readonly ProxyManager _proxyManager; public Controller() : base(ModInit.Settings) { _proxyManager = new ProxyManager(ModInit.Mikai); } [HttpGet] [Route("lite/mikai")] public async Task 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.Mikai); if (!init.enable) return Forbid(); TryEnableMagicApn(init); var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager, httpHydra); if (checksearch) { if (!IsCheckOnlineSearchEnabled()) return OnError("mikai", refresh_proxy: true); var checkResults = await invoke.Search(title, original_title, year); if (checkResults != null && checkResults.Count > 0) return Content("data-json=", "text/plain; charset=utf-8"); return OnError("mikai", refresh_proxy: true); } OnLog($"Mikai Index: title={title}, original_title={original_title}, serial={serial}, s={s}, t={t}, year={year}"); var searchResults = await invoke.Search(title, original_title, year); if (searchResults == null || searchResults.Count == 0) return OnError("mikai", refresh_proxy: true); var selected = searchResults.FirstOrDefault(); if (selected == null) return OnError("mikai", refresh_proxy: true); var details = await invoke.GetDetails(selected.Id); if (details == null || details.Players == null || details.Players.Count == 0) return OnError("mikai", refresh_proxy: true); bool isSerial = serial == 1 || (serial == -1 && !string.Equals(details.Format, "movie", StringComparison.OrdinalIgnoreCase)); var seasonDetails = await CollectSeasonDetails(details, invoke); var voices = BuildVoices(seasonDetails); if (voices.Count == 0) return OnError("mikai", refresh_proxy: true); string displayTitle = title ?? details.Details?.Names?.Name ?? original_title; if (isSerial) { MikaiVoiceInfo voiceForSeasons = null; bool restrictByVoice = !string.IsNullOrEmpty(t) && voices.TryGetValue(t, out voiceForSeasons); var seasonNumbers = restrictByVoice ? GetSeasonSet(voiceForSeasons).OrderBy(n => n).ToList() : voices.Values .SelectMany(v => GetSeasonSet(v)) .Distinct() .OrderBy(n => n) .ToList(); if (seasonNumbers.Count == 0) return OnError("mikai", refresh_proxy: true); if (s == -1) { var seasonTpl = new SeasonTpl(seasonNumbers.Count); foreach (var seasonNumber in seasonNumbers) { string link = $"{host}/lite/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}"; if (restrictByVoice) link += $"&t={HttpUtility.UrlEncode(t)}"; seasonTpl.Append($"{seasonNumber}", link, seasonNumber.ToString()); } return rjson ? Content(seasonTpl.ToJson(), "application/json; charset=utf-8") : Content(seasonTpl.ToHtml(), "text/html; charset=utf-8"); } var voicesForSeason = voices .Where(v => v.Value.Seasons.ContainsKey(s)) .ToList(); if (!voicesForSeason.Any()) return OnError("mikai", refresh_proxy: true); if (string.IsNullOrEmpty(t)) t = voicesForSeason[0].Key; else if (!voices.ContainsKey(t)) t = voicesForSeason[0].Key; var voiceTpl = new VoiceTpl(); var selectedVoiceInfo = voices[t]; var selectedSeasonSet = GetSeasonSet(selectedVoiceInfo); foreach (var voice in voicesForSeason) { var targetSeasonSet = GetSeasonSet(voice.Value); bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet); string voiceLink = $"{host}/lite/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1"; if (sameSeasonSet) voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.Key)}"; else voiceLink += $"&s=-1&t={HttpUtility.UrlEncode(voice.Key)}"; voiceTpl.Append(voice.Key, voice.Key == t, voiceLink); } if (!voices.ContainsKey(t) || !voices[t].Seasons.ContainsKey(s)) { string redirectUrl = $"{host}/lite/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s=-1&t={HttpUtility.UrlEncode(t)}"; return Redirect(redirectUrl); } var episodeTpl = new EpisodeTpl(); foreach (var ep in voices[t].Seasons[s].OrderBy(e => e.Number)) { string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {ep.Number}" : ep.Title; string streamLink = ep.Url; if (string.IsNullOrEmpty(streamLink)) continue; if (NeedsResolve(voices[t].ProviderName, streamLink)) { string callUrl = $"{host}/lite/mikai/play?url={HttpUtility.UrlEncode(streamLink)}&title={HttpUtility.UrlEncode(displayTitle)}&serial=1"; episodeTpl.Append(episodeName, displayTitle, s.ToString(), ep.Number.ToString(), accsArgs(callUrl), "call"); } else { string playUrl = BuildStreamUrl(init, streamLink, headers: null, forceProxy: false); episodeTpl.Append(episodeName, displayTitle, s.ToString(), ep.Number.ToString(), playUrl); } } episodeTpl.Append(voiceTpl); if (rjson) return Content(episodeTpl.ToJson(), "application/json; charset=utf-8"); return Content(episodeTpl.ToHtml(), "text/html; charset=utf-8"); } var movieTpl = new MovieTpl(displayTitle, original_title); foreach (var voice in voices.Values) { var episode = voice.Seasons.Values.SelectMany(v => v).OrderBy(e => e.Number).FirstOrDefault(); if (episode == null || string.IsNullOrEmpty(episode.Url)) continue; if (NeedsResolve(voice.ProviderName, episode.Url)) { if (episode.Url.Contains("ashdi.vip/vod", StringComparison.OrdinalIgnoreCase)) { var ashdiStreams = await invoke.ParseAshdiPageStreams(episode.Url); if (ashdiStreams != null && ashdiStreams.Count > 0) { foreach (var ashdiStream in ashdiStreams) { string optionName = $"{voice.DisplayName} {ashdiStream.title}"; string ashdiCallUrl = $"{host}/lite/mikai/play?url={HttpUtility.UrlEncode(ashdiStream.link)}&title={HttpUtility.UrlEncode(displayTitle)}"; movieTpl.Append(optionName, accsArgs(ashdiCallUrl), "call"); } continue; } } string callUrl = $"{host}/lite/mikai/play?url={HttpUtility.UrlEncode(episode.Url)}&title={HttpUtility.UrlEncode(displayTitle)}"; movieTpl.Append(voice.DisplayName, accsArgs(callUrl), "call"); } else { string playUrl = BuildStreamUrl(init, episode.Url, headers: null, forceProxy: false); movieTpl.Append(voice.DisplayName, playUrl); } } if (movieTpl.data == null || movieTpl.data.Count == 0) return OnError("mikai", refresh_proxy: true); return rjson ? Content(movieTpl.ToJson(), "application/json; charset=utf-8") : Content(movieTpl.ToHtml(), "text/html; charset=utf-8"); } [HttpGet("lite/mikai/play")] public async Task Play(string url, string title = null, int serial = 0) { await UpdateService.ConnectAsync(host); var init = loadKit(ModInit.Mikai); if (!init.enable) return Forbid(); TryEnableMagicApn(init); if (string.IsNullOrEmpty(url)) return OnError("mikai", refresh_proxy: true); var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager, httpHydra); OnLog($"Mikai Play: url={url}, serial={serial}"); string streamLink = await invoke.ResolveVideoUrl(url, serial == 1); if (string.IsNullOrEmpty(streamLink)) return OnError("mikai", refresh_proxy: true); List streamHeaders = null; bool forceProxy = false; if (streamLink.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase)) { streamHeaders = new List() { 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}\"}}"; return UpdateService.Validate(Content(jsonResult, "application/json; charset=utf-8")); } private async Task> CollectSeasonDetails(MikaiAnime details, MikaiInvoke invoke) { var seasonDetails = new List(); if (details == null) return seasonDetails; seasonDetails.Add(details); if (details.Relations == null || details.Relations.Count == 0) return seasonDetails; var relationIds = details.Relations .Where(r => ShouldIncludeRelation(r?.RelationType)) .Select(r => r?.Anime?.Id ?? 0) .Where(id => id > 0 && id != details.Id) .Distinct() .ToList(); foreach (var relationId in relationIds) { var relationDetails = await invoke.GetDetails(relationId); if (relationDetails?.Players == null || relationDetails.Players.Count == 0) continue; seasonDetails.Add(relationDetails); } return OrderSeasonDetails(seasonDetails); } private static bool ShouldIncludeRelation(string relationType) { if (string.IsNullOrWhiteSpace(relationType)) return false; return relationType.Equals("other", StringComparison.OrdinalIgnoreCase) || relationType.Equals("parent", StringComparison.OrdinalIgnoreCase) || relationType.Equals("sequel", StringComparison.OrdinalIgnoreCase) || relationType.Equals("prequel", StringComparison.OrdinalIgnoreCase); } private static List OrderSeasonDetails(List seasonDetails) { return seasonDetails .Where(d => d != null) .GroupBy(d => d.Id) .Select(g => g.First()) .OrderBy(d => d.Year > 0 ? d.Year : int.MaxValue) .ThenBy(d => { if (DateTime.TryParse(d.StartDate, out var parsed)) return parsed; return DateTime.MaxValue; }) .ThenBy(d => d.Id) .ToList(); } private Dictionary BuildVoices(List seasonDetails) { var voices = new Dictionary(StringComparer.OrdinalIgnoreCase); if (seasonDetails == null || seasonDetails.Count == 0) return voices; var voiceKeyMap = new Dictionary(StringComparer.OrdinalIgnoreCase); int seasonNumber = 1; foreach (var details in seasonDetails) { if (details?.Players == null || details.Players.Count == 0) { seasonNumber++; continue; } int totalProviders = details.Players.Sum(p => p?.Providers?.Count ?? 0); foreach (var player in details.Players) { if (player?.Providers == null || player.Providers.Count == 0) continue; string teamName = player.Team?.Name; if (string.IsNullOrWhiteSpace(teamName)) teamName = "Озвучка"; string baseName = player.IsSubs ? $"{teamName} (Субтитри)" : teamName; int providerIndex = 0; foreach (var provider in player.Providers) { providerIndex++; if (provider?.Episodes == null || provider.Episodes.Count == 0) continue; string displayName = baseName; if (totalProviders > 1 && !string.IsNullOrWhiteSpace(provider.Name)) displayName = $"[{provider.Name}] {displayName}"; string providerKey = string.IsNullOrWhiteSpace(provider.Name) ? $"provider-{providerIndex}" : provider.Name; string voiceKey = $"{providerKey}|{teamName}|{player.IsSubs}"; if (!voiceKeyMap.TryGetValue(voiceKey, out var voiceName)) { displayName = EnsureUniqueName(voices, displayName); voiceKeyMap[voiceKey] = displayName; voices[displayName] = new MikaiVoiceInfo { DisplayName = displayName, ProviderName = provider.Name, IsSubs = player.IsSubs }; voiceName = displayName; } var voice = voices[voiceName]; var episodes = new List(); int fallbackIndex = 1; foreach (var ep in provider.Episodes.OrderBy(e => e.Number)) { if (string.IsNullOrWhiteSpace(ep.PlayLink)) continue; int number = ep.Number > 0 ? ep.Number : fallbackIndex++; episodes.Add(new MikaiEpisodeInfo { Number = number, Title = $"Епізод {number}", Url = ep.PlayLink }); } if (episodes.Count == 0) continue; voice.Seasons[seasonNumber] = episodes; } } seasonNumber++; } return voices; } private static string EnsureUniqueName(Dictionary voices, string name) { if (!voices.ContainsKey(name)) return name; int index = 2; string candidate = $"{name} {index}"; while (voices.ContainsKey(candidate)) { index++; candidate = $"{name} {index}"; } return candidate; } private static bool NeedsResolve(string providerName, string streamLink) { if (!string.IsNullOrEmpty(providerName)) { if (providerName.Equals("ASHDI", StringComparison.OrdinalIgnoreCase) || providerName.Equals("MOONANIME", StringComparison.OrdinalIgnoreCase)) return true; } return streamLink.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase) || streamLink.Contains("moonanime.art", StringComparison.OrdinalIgnoreCase); } private static HashSet GetSeasonSet(MikaiVoiceInfo voice) { if (voice?.Seasons == null || voice.Seasons.Count == 0) return new HashSet(); return voice.Seasons .Where(kv => kv.Value != null && kv.Value.Any(ep => !string.IsNullOrEmpty(ep.Url))) .Select(kv => kv.Key) .ToHashSet(); } 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 string BuildStreamUrl(OnlinesSettings init, string streamLink, List 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($"Mikai: увімкнено 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); } } }