using Microsoft.AspNetCore.Mvc; using MoonAnime.Models; using Shared; using Shared.Engine; using Shared.Models; using Shared.Models.Online.Settings; using Shared.Models.Templates; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; namespace MoonAnime.Controllers { public class Controller : BaseOnlineController { private readonly ProxyManager proxyManager; public Controller() : base(ModInit.Settings) { proxyManager = new ProxyManager(ModInit.MoonAnime); } [HttpGet] [Route("moonanime")] 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 mal_id, string t, int s = -1, bool rjson = false, bool checksearch = false) { await UpdateService.ConnectAsync(host); var init = await loadKit(ModInit.MoonAnime); if (!init.enable) return Forbid(); var invoke = new MoonAnimeInvoke(init, hybridCache, OnLog, proxyManager); if (checksearch) { if (AppInit.conf?.online?.checkOnlineSearch != true) return OnError("moonanime", proxyManager); var checkResults = await invoke.Search(imdb_id, mal_id, title, original_title, year); if (checkResults != null && checkResults.Count > 0) return Content("data-json=", "text/plain; charset=utf-8"); return OnError("moonanime", proxyManager); } OnLog($"MoonAnime: title={title}, original_title={original_title}, imdb={imdb_id}, mal_id={mal_id}, serial={serial}, s={s}, t={t}"); var seasons = await invoke.Search(imdb_id, mal_id, title, original_title, year); if (seasons == null || seasons.Count == 0) return OnError("moonanime", proxyManager); bool isSeries = serial == 1; MoonAnimeSeasonContent firstSeasonData = null; if (serial == -1) { firstSeasonData = await invoke.GetSeasonContent(seasons[0]); if (firstSeasonData == null || firstSeasonData.Voices.Count == 0) return OnError("moonanime", proxyManager); isSeries = firstSeasonData.IsSeries; } if (isSeries) { return await RenderSerial(invoke, seasons, imdb_id, kinopoisk_id, title, original_title, year, mal_id, s, t, rjson); } return await RenderMovie(invoke, seasons, title, original_title, firstSeasonData, rjson); } [HttpGet("moonanime/play")] public async Task Play(string file, string title = null) { await UpdateService.ConnectAsync(host); var init = await loadKit(ModInit.MoonAnime); if (!init.enable) return Forbid(); if (string.IsNullOrWhiteSpace(file)) return OnError("moonanime", proxyManager); var invoke = new MoonAnimeInvoke(init, hybridCache, OnLog, proxyManager); var streams = invoke.ParseStreams(file); if (streams == null || streams.Count == 0) return OnError("moonanime", proxyManager); if (streams.Count == 1) { string singleUrl = BuildStreamUrl(init, streams[0].Url); string singleJson = VideoTpl.ToJson("play", singleUrl, title ?? string.Empty, quality: streams[0].Quality ?? "auto"); return UpdateService.Validate(Content(singleJson, "application/json; charset=utf-8")); } var streamQuality = new StreamQualityTpl(); foreach (var stream in streams) { string streamUrl = BuildStreamUrl(init, stream.Url); streamQuality.Append(streamUrl, stream.Quality); } if (!streamQuality.Any()) return OnError("moonanime", proxyManager); var first = streamQuality.Firts(); string json = VideoTpl.ToJson("play", first.link, title ?? string.Empty, streamquality: streamQuality); return UpdateService.Validate(Content(json, "application/json; charset=utf-8")); } private async Task RenderSerial( MoonAnimeInvoke invoke, List seasons, string imdbId, long kinopoiskId, string title, string originalTitle, int year, string malId, int selectedSeason, string selectedVoice, bool rjson) { var orderedSeasons = seasons .Where(s => s != null && !string.IsNullOrWhiteSpace(s.Url)) .OrderBy(s => s.SeasonNumber) .ToList(); if (orderedSeasons.Count == 0) return OnError("moonanime", proxyManager); if (selectedSeason == -1) { var seasonTpl = new SeasonTpl(orderedSeasons.Count); foreach (var season in orderedSeasons) { int seasonNumber = season.SeasonNumber <= 0 ? 1 : season.SeasonNumber; string seasonName = $"Сезон {seasonNumber}"; string seasonLink = BuildIndexUrl(imdbId, kinopoiskId, title, originalTitle, year, 1, malId, seasonNumber, selectedVoice); seasonTpl.Append(seasonName, seasonLink, seasonNumber); } return rjson ? Content(seasonTpl.ToJson(), "application/json; charset=utf-8") : Content(seasonTpl.ToHtml(), "text/html; charset=utf-8"); } var currentSeason = orderedSeasons.FirstOrDefault(s => s.SeasonNumber == selectedSeason) ?? orderedSeasons[0]; var seasonData = await invoke.GetSeasonContent(currentSeason); if (seasonData == null) return OnError("moonanime", proxyManager); var voices = seasonData.Voices .Where(v => v != null && v.Episodes != null && v.Episodes.Count > 0) .ToList(); if (voices.Count == 0) return OnError("moonanime", proxyManager); int activeVoiceIndex = ParseVoiceIndex(selectedVoice, voices.Count); var voiceTpl = new VoiceTpl(voices.Count); for (int i = 0; i < voices.Count; i++) { string voiceName = string.IsNullOrWhiteSpace(voices[i].Name) ? $"Озвучка {i + 1}" : voices[i].Name; string voiceLink = BuildIndexUrl(imdbId, kinopoiskId, title, originalTitle, year, 1, malId, currentSeason.SeasonNumber, i.ToString()); voiceTpl.Append(voiceName, i == activeVoiceIndex, voiceLink); } var selectedVoiceData = voices[activeVoiceIndex]; var episodes = selectedVoiceData.Episodes .Where(e => e != null && !string.IsNullOrWhiteSpace(e.File)) .OrderBy(e => e.Number <= 0 ? int.MaxValue : e.Number) .ThenBy(e => e.Name) .ToList(); if (episodes.Count == 0) return OnError("moonanime", proxyManager); string displayTitle = !string.IsNullOrWhiteSpace(title) ? title : !string.IsNullOrWhiteSpace(originalTitle) ? originalTitle : "MoonAnime"; var episodeTpl = new EpisodeTpl(episodes.Count); foreach (var episode in episodes) { int episodeNumber = episode.Number <= 0 ? 1 : episode.Number; string episodeName = string.IsNullOrWhiteSpace(episode.Name) ? $"Епізод {episodeNumber}" : episode.Name; string callUrl = $"{host}/moonanime/play?file={HttpUtility.UrlEncode(episode.File)}&title={HttpUtility.UrlEncode(displayTitle)}"; episodeTpl.Append(episodeName, displayTitle, currentSeason.SeasonNumber.ToString(), episodeNumber.ToString(), accsArgs(callUrl), "call"); } episodeTpl.Append(voiceTpl); return rjson ? Content(episodeTpl.ToJson(), "application/json; charset=utf-8") : Content(episodeTpl.ToHtml(), "text/html; charset=utf-8"); } private async Task RenderMovie( MoonAnimeInvoke invoke, List seasons, string title, string originalTitle, MoonAnimeSeasonContent firstSeasonData, bool rjson) { var currentSeason = seasons .Where(s => s != null && !string.IsNullOrWhiteSpace(s.Url)) .OrderBy(s => s.SeasonNumber) .FirstOrDefault(); if (currentSeason == null) return OnError("moonanime", proxyManager); MoonAnimeSeasonContent seasonData = firstSeasonData; if (seasonData == null || !string.Equals(seasonData.Url, currentSeason.Url, StringComparison.OrdinalIgnoreCase)) seasonData = await invoke.GetSeasonContent(currentSeason); if (seasonData == null || seasonData.Voices.Count == 0) return OnError("moonanime", proxyManager); string displayTitle = !string.IsNullOrWhiteSpace(title) ? title : !string.IsNullOrWhiteSpace(originalTitle) ? originalTitle : "MoonAnime"; var movieTpl = new MovieTpl(displayTitle, originalTitle); int fallbackIndex = 1; foreach (var voice in seasonData.Voices) { if (voice == null) continue; string file = !string.IsNullOrWhiteSpace(voice.MovieFile) ? voice.MovieFile : voice.Episodes?.FirstOrDefault(e => !string.IsNullOrWhiteSpace(e.File))?.File; if (string.IsNullOrWhiteSpace(file)) continue; string voiceName = string.IsNullOrWhiteSpace(voice.Name) ? $"Озвучка {fallbackIndex}" : voice.Name; string callUrl = $"{host}/moonanime/play?file={HttpUtility.UrlEncode(file)}&title={HttpUtility.UrlEncode(displayTitle)}"; movieTpl.Append(voiceName, accsArgs(callUrl), "call"); fallbackIndex++; } if (movieTpl.IsEmpty) return OnError("moonanime", proxyManager); return rjson ? Content(movieTpl.ToJson(), "application/json; charset=utf-8") : Content(movieTpl.ToHtml(), "text/html; charset=utf-8"); } private string BuildIndexUrl(string imdbId, long kinopoiskId, string title, string originalTitle, int year, int serial, string malId, int season, string voice) { var url = new StringBuilder(); url.Append($"{host}/moonanime?imdb_id={HttpUtility.UrlEncode(imdbId)}"); url.Append($"&kinopoisk_id={kinopoiskId}"); url.Append($"&title={HttpUtility.UrlEncode(title)}"); url.Append($"&original_title={HttpUtility.UrlEncode(originalTitle)}"); url.Append($"&year={year}"); url.Append($"&serial={serial}"); if (!string.IsNullOrWhiteSpace(malId)) url.Append($"&mal_id={HttpUtility.UrlEncode(malId)}"); if (season > 0) url.Append($"&s={season}"); if (!string.IsNullOrWhiteSpace(voice)) url.Append($"&t={HttpUtility.UrlEncode(voice)}"); return url.ToString(); } private int ParseVoiceIndex(string voiceValue, int totalVoices) { if (totalVoices <= 0) return 0; if (!int.TryParse(voiceValue, out int index)) return 0; if (index < 0 || index >= totalVoices) return 0; return index; } private string BuildStreamUrl(OnlinesSettings init, string streamLink) { string link = StripLampacArgs(streamLink?.Trim()); if (string.IsNullOrEmpty(link)) return link; var headers = new List { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://moonanime.art/") }; if (ApnHelper.IsEnabled(init)) { if (ModInit.ApnHostProvided) return ApnHelper.WrapUrl(init, link); var noApn = (OnlinesSettings)init.Clone(); noApn.apnstream = false; noApn.apn = null; return HostStreamProxy(noApn, link, headers: headers, proxy: proxyManager.Get()); } return HostStreamProxy(init, link, headers: headers, proxy: proxyManager.Get()); } private static string StripLampacArgs(string url) { if (string.IsNullOrEmpty(url)) return url; string cleaned = Regex.Replace( url, @"([?&])(account_email|uid|nws_id)=[^&]*", "$1", RegexOptions.IgnoreCase ); cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&'); return cleaned; } } }