using Shared.Engine; using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Linq; using System.Web; using Shared; using Shared.Models.Templates; using Shared.Models.Online.Settings; using Shared.Models; using Makhno.Models; namespace Makhno { [Route("makhno")] public class MakhnoController : BaseOnlineController { private readonly ProxyManager proxyManager; public MakhnoController() : base(ModInit.Settings) { proxyManager = new ProxyManager(ModInit.Makhno); } [HttpGet] 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, int season = -1, bool rjson = false, bool checksearch = false) { if (checksearch) return Content("data-json="); await UpdateService.ConnectAsync(host); var init = await loadKit(ModInit.Makhno); if (!init.enable) return OnError(); Initialization(init); OnLog($"Makhno: {title} (serial={serial}, s={s}, season={season}, t={t})"); var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager); var resolved = await ResolvePlaySource(imdb_id, title, original_title, year, serial, invoke); if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl)) return OnError(); if (resolved.ShouldEnrich) { _ = Task.Run(async () => { try { await EnrichWormhole(imdb_id, title, original_title, year, resolved, invoke); } catch (Exception ex) { OnLog($"Makhno wormhole enrich failed: {ex.Message}"); } }); } if (resolved.IsSerial) return await HandleSerial(resolved.PlayUrl, imdb_id, title, original_title, year, t, season, rjson, invoke); return await HandleMovie(resolved.PlayUrl, imdb_id, title, original_title, year, rjson, invoke); } [HttpGet] [Route("play")] public async Task Play(long id, string imdb_id, long kinopoisk_id, string title, string original_title, int year, int s, int season, string t, string episodeId, bool play = false, bool rjson = false) { await UpdateService.ConnectAsync(host); var init = await loadKit(ModInit.Makhno); if (!init.enable) return OnError(); Initialization(init); OnLog($"Makhno Play: {title} (s={s}, season={season}, t={t}, episodeId={episodeId}) play={play}"); var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager); var resolved = await ResolvePlaySource(imdb_id, title, original_title, year, serial: 1, invoke); if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl)) return OnError(); var playerData = await InvokeCache($"makhno:player:{resolved.PlayUrl}", TimeSpan.FromMinutes(10), async () => { return await invoke.GetPlayerData(resolved.PlayUrl); }); if (playerData?.Voices == null || !playerData.Voices.Any()) return OnError(); if (string.IsNullOrEmpty(t) || !int.TryParse(t, out int voiceIndex) || voiceIndex >= playerData.Voices.Count) return OnError(); var selectedVoice = playerData.Voices[voiceIndex]; if (season < 0 || season >= selectedVoice.Seasons.Count) return OnError(); var selectedSeason = selectedVoice.Seasons[season]; foreach (var episode in selectedSeason.Episodes) { if (episode.Id == episodeId && !string.IsNullOrEmpty(episode.File)) { OnLog($"Makhno Play: Found episode {episode.Title}, stream: {episode.File}"); string streamUrl = BuildStreamUrl(init, episode.File); string episodeTitle = $"{title ?? original_title} - {episode.Title}"; if (play) return UpdateService.Validate(Redirect(streamUrl)); return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, episodeTitle), "application/json; charset=utf-8")); } } OnLog("Makhno Play: Episode not found"); return OnError(); } [HttpGet] [Route("play/movie")] public async Task PlayMovie(long id, string imdb_id, string title, string original_title, int year, bool play = false, bool rjson = false) { await UpdateService.ConnectAsync(host); var init = await loadKit(ModInit.Makhno); if (!init.enable) return OnError(); Initialization(init); OnLog($"Makhno PlayMovie: {title} ({year}) play={play}"); var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager); var resolved = await ResolvePlaySource(imdb_id, title, original_title, year, serial: 0, invoke); if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl)) return OnError(); var playerData = await InvokeCache($"makhno:player:{resolved.PlayUrl}", TimeSpan.FromMinutes(10), async () => { return await invoke.GetPlayerData(resolved.PlayUrl); }); if (playerData?.File == null) return OnError(); string streamUrl = BuildStreamUrl(init, playerData.File); if (play) return UpdateService.Validate(Redirect(streamUrl)); return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? original_title), "application/json; charset=utf-8")); } private async Task HandleMovie(string playUrl, string imdb_id, string title, string original_title, int year, bool rjson, MakhnoInvoke invoke) { var init = ModInit.Makhno; var playerData = await InvokeCache($"makhno:player:{playUrl}", TimeSpan.FromMinutes(10), async () => { return await invoke.GetPlayerData(playUrl); }); if (playerData?.File == null) return OnError(); string movieLink = $"{host}/makhno/play/movie?imdb_id={HttpUtility.UrlEncode(imdb_id)}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&play=true"; var tpl = new MovieTpl(title ?? original_title, original_title, 1); tpl.Append(title ?? original_title, accsArgs(movieLink), method: "play"); return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8"); } private async Task HandleSerial(string playUrl, string imdb_id, string title, string original_title, int year, string t, int season, bool rjson, MakhnoInvoke invoke) { var init = ModInit.Makhno; var playerData = await InvokeCache($"makhno:player:{playUrl}", TimeSpan.FromMinutes(10), async () => { return await invoke.GetPlayerData(playUrl); }); if (playerData?.Voices == null || !playerData.Voices.Any()) return OnError(); if (season == -1) { var firstVoice = playerData.Voices.First(); var season_tpl = new SeasonTpl(); for (int i = 0; i < firstVoice.Seasons.Count; i++) { var seasonItem = firstVoice.Seasons[i]; string seasonName = seasonItem.Title ?? $"Сезон {i + 1}"; string link = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={i}"; season_tpl.Append(seasonName, link, i.ToString()); } return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8"); } if (season < 0 || season >= playerData.Voices.First().Seasons.Count) return OnError(); var voice_tpl = new VoiceTpl(); var episode_tpl = new EpisodeTpl(); string selectedVoice = t; if (string.IsNullOrEmpty(selectedVoice) && playerData.Voices.Any()) { selectedVoice = "0"; } for (int i = 0; i < playerData.Voices.Count; i++) { var voice = playerData.Voices[i]; string voiceName = voice.Name ?? $"Озвучка {i + 1}"; string voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={season}&t={i}"; bool isActive = selectedVoice == i.ToString(); voice_tpl.Append(voiceName, isActive, voiceLink); } if (!string.IsNullOrEmpty(selectedVoice) && int.TryParse(selectedVoice, out int voiceIndex) && voiceIndex < playerData.Voices.Count) { var selectedVoiceData = playerData.Voices[voiceIndex]; if (season < selectedVoiceData.Seasons.Count) { var selectedSeason = selectedVoiceData.Seasons[season]; var sortedEpisodes = selectedSeason.Episodes.OrderBy(e => ExtractEpisodeNumber(e.Title)).ToList(); for (int i = 0; i < sortedEpisodes.Count; i++) { var episode = sortedEpisodes[i]; if (!string.IsNullOrEmpty(episode.File)) { string episodeLink = $"{host}/makhno/play?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&season={season}&t={selectedVoice}&episodeId={episode.Id}"; episode_tpl.Append(episode.Title, title ?? original_title, season.ToString(), (i + 1).ToString("D2"), episodeLink, "call"); } } } } 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"); } private int ExtractEpisodeNumber(string title) { if (string.IsNullOrEmpty(title)) return 0; var match = System.Text.RegularExpressions.Regex.Match(title, @"(\d+)"); return match.Success ? int.Parse(match.Groups[1].Value) : 0; } private async Task ResolvePlaySource(string imdbId, string title, string originalTitle, int year, int serial, MakhnoInvoke invoke) { string playUrl = null; if (!string.IsNullOrEmpty(imdbId)) { string cacheKey = $"makhno:wormhole:{imdbId}"; playUrl = await InvokeCache(cacheKey, TimeSpan.FromMinutes(5), async () => { return await invoke.GetWormholePlay(imdbId); }); if (!string.IsNullOrEmpty(playUrl)) { return new ResolveResult { PlayUrl = playUrl, IsSerial = IsSerialByUrl(playUrl, serial), ShouldEnrich = false }; } } string searchQuery = originalTitle ?? title; string searchCacheKey = $"makhno:uatut:search:{imdbId ?? searchQuery}"; var searchResults = await InvokeCache>(searchCacheKey, TimeSpan.FromMinutes(10), async () => { return await invoke.SearchUaTUT(searchQuery, imdbId); }); if (searchResults == null || searchResults.Count == 0) return null; var selected = invoke.SelectUaTUTItem(searchResults, imdbId, year > 0 ? year : null, title, originalTitle); if (selected == null) return null; var ashdiPath = await InvokeCache($"makhno:ashdi:{selected.Id}", TimeSpan.FromMinutes(10), async () => { return await invoke.GetAshdiPath(selected.Id); }); if (string.IsNullOrEmpty(ashdiPath)) return null; playUrl = invoke.BuildAshdiUrl(ashdiPath); bool isSerial = serial == 1 || IsSerialByCategory(selected.Category) || IsSerialByUrl(playUrl, serial); return new ResolveResult { PlayUrl = playUrl, AshdiPath = ashdiPath, Selected = selected, IsSerial = isSerial, ShouldEnrich = true }; } private bool IsSerialByCategory(string category) { if (string.IsNullOrWhiteSpace(category)) return false; return category.Equals("Серіал", StringComparison.OrdinalIgnoreCase) || category.Equals("Аніме", StringComparison.OrdinalIgnoreCase); } private bool IsSerialByUrl(string url, int serial) { if (serial == 1) return true; if (string.IsNullOrEmpty(url)) return false; return url.Contains("/serial/", StringComparison.OrdinalIgnoreCase); } private async Task EnrichWormhole(string imdbId, string title, string originalTitle, int year, ResolveResult resolved, MakhnoInvoke invoke) { if (string.IsNullOrWhiteSpace(imdbId) || resolved?.Selected == null || string.IsNullOrWhiteSpace(resolved.AshdiPath)) return; int? yearValue = year > 0 ? year : null; if (!yearValue.HasValue && int.TryParse(resolved.Selected.Year, out int parsedYear)) yearValue = parsedYear; var tmdbResult = await invoke.FetchTmdbByImdb(imdbId, yearValue); if (tmdbResult == null) return; var (item, mediaType) = tmdbResult.Value; var tmdbId = item.Value("id"); if (!tmdbId.HasValue) return; string original = item.Value("original_title") ?? item.Value("original_name") ?? resolved.Selected.TitleEn ?? originalTitle ?? title; string resultTitle = resolved.Selected.Title ?? item.Value("title") ?? item.Value("name"); var payload = new { imdb_id = imdbId, _id = $"{mediaType}:{tmdbId.Value}", original_title = original, title = resultTitle, serial = mediaType == "tv" ? 1 : 0, ashdi = resolved.AshdiPath, year = (resolved.Selected.Year ?? yearValue?.ToString()) }; await invoke.PostWormholeAsync(payload); } private string BuildStreamUrl(OnlinesSettings init, string streamLink) { string link = accsArgs(streamLink); 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 class ResolveResult { public string PlayUrl { get; set; } public string AshdiPath { get; set; } public SearchResult Selected { get; set; } public bool IsSerial { get; set; } public bool ShouldEnrich { get; set; } } } }