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()) { OnLog("Makhno Play: no voices parsed"); return OnError(); } if (string.IsNullOrEmpty(t) || !int.TryParse(t, out int voiceIndex) || voiceIndex >= playerData.Voices.Count) return OnError(); var selectedVoice = playerData.Voices[voiceIndex]; int seasonIndex = season > 0 ? season - 1 : season; if (seasonIndex < 0 || seasonIndex >= selectedVoice.Seasons.Count) return OnError(); var selectedSeason = selectedVoice.Seasons[seasonIndex]; 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) { OnLog("Makhno PlayMovie: no file parsed"); 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) { OnLog("Makhno HandleMovie: no file parsed"); 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()) { OnLog("Makhno HandleSerial: no voices parsed"); return OnError(); } var voiceSeasons = playerData.Voices .Select((voice, index) => new { Voice = voice, Index = index, Seasons = GetSeasonsWithNumbers(voice) }) .Where(v => v.Seasons.Count > 0) .ToList(); OnLog($"Makhno SeasonDebug: voices={playerData.Voices.Count}, withSeasons={voiceSeasons.Count}, t={t}, season={season}"); foreach (var v in voiceSeasons) { var seasonList = string.Join(", ", v.Seasons.Select(s => $"{s.Number}:{s.Season?.Title}")); OnLog($"Makhno SeasonDebug: voice[{v.Index}]='{v.Voice?.Name}', seasons=[{seasonList}]"); } var seasonNumbers = voiceSeasons .SelectMany(v => v.Seasons.Select(s => s.Number)) .Distinct() .OrderBy(n => n) .ToList(); if (seasonNumbers.Count == 0) return OnError(); if (season == -1) { if (int.TryParse(t, out int seasonVoiceIndex) && seasonVoiceIndex >= 0 && seasonVoiceIndex < playerData.Voices.Count) { var seasonsForVoice = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndex]) .Select(s => s.Number) .Distinct() .OrderBy(n => n) .ToList(); if (seasonsForVoice.Count > 0) seasonNumbers = seasonsForVoice; } var season_tpl = new SeasonTpl(); foreach (var seasonNumber in seasonNumbers) { var seasonItem = voiceSeasons .SelectMany(v => v.Seasons) .FirstOrDefault(s => s.Number == seasonNumber); var preferredVoice = voiceSeasons.FirstOrDefault(v => v.Seasons.Any(s => s.Number == seasonNumber)); string voiceParam = preferredVoice != null ? $"&t={preferredVoice.Index}" : string.Empty; string seasonName = seasonItem.Season?.Title ?? $"Сезон {seasonNumber}"; string link = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}{voiceParam}"; season_tpl.Append(seasonName, link, seasonNumber.ToString()); } return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8"); } var voice_tpl = new VoiceTpl(); var episode_tpl = new EpisodeTpl(); int requestedSeason = seasonNumbers.Contains(season) ? season : seasonNumbers.First(); string selectedVoice = t; if (string.IsNullOrEmpty(selectedVoice) || !int.TryParse(selectedVoice, out _)) { var voiceWithSeason = voiceSeasons.FirstOrDefault(v => v.Seasons.Any(s => s.Number == requestedSeason)); selectedVoice = voiceWithSeason != null ? voiceWithSeason.Index.ToString() : voiceSeasons.First().Index.ToString(); } for (int i = 0; i < playerData.Voices.Count; i++) { var voice = playerData.Voices[i]; string voiceName = voice.Name ?? $"Озвучка {i + 1}"; var seasonsForVoice = GetSeasonsWithNumbers(voice); if (seasonsForVoice.Count == 0) continue; string voiceLink; if (seasonsForVoice.Count > 1) { // Always show season list for multi-season voices to keep filter correct voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season=-1&t={i}"; } else { int onlySeason = seasonsForVoice[0].Number; voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={onlySeason}&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]; var seasonsForVoice = GetSeasonsWithNumbers(selectedVoiceData); if (seasonsForVoice.Count > 0) { int effectiveSeasonNumber = seasonsForVoice.Any(s => s.Number == requestedSeason) ? requestedSeason : seasonsForVoice.Min(s => s.Number); if (effectiveSeasonNumber != season) { string redirectUrl = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={effectiveSeasonNumber}&t={voiceIndex}"; return UpdateService.Validate(Redirect(redirectUrl)); } var selectedSeason = seasonsForVoice.First(s => s.Number == effectiveSeasonNumber).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 streamUrl = BuildStreamUrl(init, episode.File); episode_tpl.Append( episode.Title, title ?? original_title, effectiveSeasonNumber.ToString(), (i + 1).ToString("D2"), streamUrl ); } } } } 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 int? ExtractSeasonNumber(string title) { if (string.IsNullOrEmpty(title)) return null; var match = System.Text.RegularExpressions.Regex.Match(title, @"(\d+)"); return match.Success ? int.Parse(match.Groups[1].Value) : (int?)null; } private List<(Season Season, int Number)> GetSeasonsWithNumbers(Voice voice) { var result = new List<(Season Season, int Number)>(); if (voice?.Seasons == null || voice.Seasons.Count == 0) return result; for (int i = 0; i < voice.Seasons.Count; i++) { var season = voice.Seasons[i]; int number = ExtractSeasonNumber(season?.Title) ?? (i + 1); result.Add((season, number)); } return result; } 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, serial) || IsSerialByUrl(playUrl, serial); return new ResolveResult { PlayUrl = playUrl, AshdiPath = ashdiPath, Selected = selected, IsSerial = isSerial, ShouldEnrich = true }; } private bool IsSerialByCategory(string category, int serial) { if (string.IsNullOrWhiteSpace(category)) return false; if (category.Equals("Аніме", StringComparison.OrdinalIgnoreCase) || category.Equals("Аниме", StringComparison.OrdinalIgnoreCase)) { return serial == 1; } return category.Equals("Серіал", StringComparison.OrdinalIgnoreCase) || category.Equals("Сериал", StringComparison.OrdinalIgnoreCase) || category.Equals("Аніме", StringComparison.OrdinalIgnoreCase) || category.Equals("Аниме", StringComparison.OrdinalIgnoreCase) || category.Equals("Мультсеріал", StringComparison.OrdinalIgnoreCase) || category.Equals("Мультсериал", StringComparison.OrdinalIgnoreCase) || category.Equals("TV", 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 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) { 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); } 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; } } } }