From 5f40e1781f88137327b68af2ee87bbcee47cf8fa Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 19:30:42 +0200 Subject: [PATCH 01/37] feat(makhno): add Makhno online streaming module Integrate Makhno video streaming service with support for movies and serials. The module provides search functionality, player data retrieval, and streaming capabilities through multiple external APIs including Wormhole, Ashdi, and UaTUT. Features include: - HTTP controller for handling playback requests - Support for multiple voice translations and seasons - Proxy management and caching - TMDB integration for metadata enrichment - Online API integration for event handling --- Makhno/Controller.cs | 403 +++++++++++++++++++++++ Makhno/Makhno.csproj | 15 + Makhno/MakhnoInvoke.cs | 592 ++++++++++++++++++++++++++++++++++ Makhno/ModInit.cs | 196 +++++++++++ Makhno/Models/MakhnoModels.cs | 61 ++++ Makhno/OnlineApi.cs | 40 +++ Makhno/manifest.json | 6 + 7 files changed, 1313 insertions(+) create mode 100644 Makhno/Controller.cs create mode 100644 Makhno/Makhno.csproj create mode 100644 Makhno/MakhnoInvoke.cs create mode 100644 Makhno/ModInit.cs create mode 100644 Makhno/Models/MakhnoModels.cs create mode 100644 Makhno/OnlineApi.cs create mode 100644 Makhno/manifest.json diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs new file mode 100644 index 0000000..4b93362 --- /dev/null +++ b/Makhno/Controller.cs @@ -0,0 +1,403 @@ +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) + { + 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; } + } + } +} diff --git a/Makhno/Makhno.csproj b/Makhno/Makhno.csproj new file mode 100644 index 0000000..1fbe365 --- /dev/null +++ b/Makhno/Makhno.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + library + true + + + + + ..\..\Shared.dll + + + + diff --git a/Makhno/MakhnoInvoke.cs b/Makhno/MakhnoInvoke.cs new file mode 100644 index 0000000..c70513f --- /dev/null +++ b/Makhno/MakhnoInvoke.cs @@ -0,0 +1,592 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Shared; +using Shared.Engine; +using Shared.Models; +using Shared.Models.Online.Settings; +using Makhno.Models; + +namespace Makhno +{ + public class MakhnoInvoke + { + private const string WormholeHost = "http://wormhole.lampame.v6.rocks/"; + private const string AshdiHost = "https://ashdi.vip"; + + private readonly OnlinesSettings _init; + private readonly IHybridCache _hybridCache; + private readonly Action _onLog; + private readonly ProxyManager _proxyManager; + + public MakhnoInvoke(OnlinesSettings init, IHybridCache hybridCache, Action onLog, ProxyManager proxyManager) + { + _init = init; + _hybridCache = hybridCache; + _onLog = onLog; + _proxyManager = proxyManager; + } + + public async Task GetWormholePlay(string imdbId) + { + if (string.IsNullOrWhiteSpace(imdbId)) + return null; + + string url = $"{WormholeHost}?imdb_id={imdbId}"; + try + { + var headers = new List() + { + new HeadersModel("User-Agent", Http.UserAgent) + }; + + string response = await Http.Get(url, timeoutSeconds: 4, headers: headers, proxy: _proxyManager.Get()); + if (string.IsNullOrWhiteSpace(response)) + return null; + + var payload = JsonConvert.DeserializeObject(response); + return string.IsNullOrWhiteSpace(payload?.play) ? null : payload.play; + } + catch (Exception ex) + { + _onLog($"Makhno wormhole error: {ex.Message}"); + return null; + } + } + + public async Task> SearchUaTUT(string query, string imdbId = null) + { + try + { + string searchUrl = $"{_init.apihost}/search.php"; + + if (!string.IsNullOrEmpty(imdbId)) + { + var imdbResults = await PerformSearch(searchUrl, imdbId); + if (imdbResults?.Any() == true) + return imdbResults; + } + + if (!string.IsNullOrEmpty(query)) + { + var titleResults = await PerformSearch(searchUrl, query); + return titleResults ?? new List(); + } + + return new List(); + } + catch (Exception ex) + { + _onLog($"Makhno UaTUT search error: {ex.Message}"); + return new List(); + } + } + + private async Task> PerformSearch(string searchUrl, string query) + { + string url = $"{searchUrl}?q={WebUtility.UrlEncode(query)}"; + _onLog($"Makhno UaTUT searching: {url}"); + + var headers = new List() + { + new HeadersModel("User-Agent", Http.UserAgent) + }; + + var response = await Http.Get(url, headers: headers, proxy: _proxyManager.Get()); + + if (string.IsNullOrEmpty(response)) + return null; + + try + { + var results = JsonConvert.DeserializeObject>(response); + _onLog($"Makhno UaTUT found {results?.Count ?? 0} results for query: {query}"); + return results; + } + catch (Exception ex) + { + _onLog($"Makhno UaTUT parse error: {ex.Message}"); + return null; + } + } + + public async Task GetMoviePageContent(string movieId) + { + try + { + string url = $"{_init.apihost}/{movieId}"; + _onLog($"Makhno UaTUT getting movie page: {url}"); + + var headers = new List() + { + new HeadersModel("User-Agent", Http.UserAgent) + }; + var response = await Http.Get(url, headers: headers, proxy: _proxyManager.Get()); + + return response; + } + catch (Exception ex) + { + _onLog($"Makhno UaTUT GetMoviePageContent error: {ex.Message}"); + return null; + } + } + + public string GetPlayerUrl(string moviePageContent) + { + try + { + if (string.IsNullOrEmpty(moviePageContent)) + return null; + + var match = Regex.Match(moviePageContent, @"]*id=[""']vip-player[""'][^>]*src=[""']([^""']+)", RegexOptions.IgnoreCase); + if (match.Success) + return NormalizePlayerUrl(match.Groups[1].Value); + + match = Regex.Match(moviePageContent, @"]*id=[""']alt-player[""'][^>]*src=[""']([^""']+)", RegexOptions.IgnoreCase); + if (match.Success) + return NormalizePlayerUrl(match.Groups[1].Value); + + var iframeMatches = Regex.Matches(moviePageContent, @"]*(?:id=[""']([^""']+)[""'])?[^>]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase); + foreach (Match iframe in iframeMatches) + { + string iframeId = iframe.Groups[1].Value?.ToLowerInvariant(); + string src = iframe.Groups[2].Value; + if (string.IsNullOrEmpty(src)) + continue; + + if (!string.IsNullOrEmpty(iframeId) && iframeId.Contains("player")) + return NormalizePlayerUrl(src); + + if (src.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase) || + src.Contains("zetvideo.net", StringComparison.OrdinalIgnoreCase) || + src.Contains("player", StringComparison.OrdinalIgnoreCase)) + return NormalizePlayerUrl(src); + } + + var urlMatch = Regex.Match(moviePageContent, @"(https?://[^\"'\s>]+/(?:vod|serial)/\d+[^\"'\s>]*)", RegexOptions.IgnoreCase); + if (urlMatch.Success) + return NormalizePlayerUrl(urlMatch.Groups[1].Value); + + return null; + } + catch (Exception ex) + { + _onLog($"Makhno UaTUT GetPlayerUrl error: {ex.Message}"); + return null; + } + } + + private string NormalizePlayerUrl(string src) + { + if (string.IsNullOrEmpty(src)) + return null; + + if (src.StartsWith("//")) + return $"https:{src}"; + + return src; + } + + public async Task GetPlayerData(string playerUrl) + { + if (string.IsNullOrEmpty(playerUrl)) + return null; + + try + { + string requestUrl = playerUrl; + var headers = new List() + { + new HeadersModel("User-Agent", Http.UserAgent) + }; + + if (playerUrl.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase)) + { + headers.Add(new HeadersModel("Referer", "https://ashdi.vip/")); + } + + if (ApnHelper.IsAshdiUrl(playerUrl) && ApnHelper.IsEnabled(_init)) + requestUrl = ApnHelper.WrapUrl(_init, playerUrl); + + _onLog($"Makhno getting player data from: {requestUrl}"); + + var response = await Http.Get(requestUrl, headers: headers, proxy: _proxyManager.Get()); + if (string.IsNullOrEmpty(response)) + return null; + + return ParsePlayerData(response); + } + catch (Exception ex) + { + _onLog($"Makhno GetPlayerData error: {ex.Message}"); + return null; + } + } + + private PlayerData ParsePlayerData(string html) + { + try + { + if (string.IsNullOrEmpty(html)) + return null; + + var fileMatch = Regex.Match(html, @"file:'([^']+)'", RegexOptions.IgnoreCase); + if (!fileMatch.Success) + fileMatch = Regex.Match(html, @"file:\s*\"([^\"]+)\"", RegexOptions.IgnoreCase); + + if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("[")) + { + var posterMatch = Regex.Match(html, @"poster:[\"']([^\"']+)[\"']", RegexOptions.IgnoreCase); + return new PlayerData + { + File = fileMatch.Groups[1].Value, + Poster = posterMatch.Success ? posterMatch.Groups[1].Value : null, + Voices = new List() + }; + } + + var m3u8Match = Regex.Match(html, @"(https?://[^\"'\s>]+\.m3u8[^\"'\s>]*)", RegexOptions.IgnoreCase); + if (m3u8Match.Success) + { + return new PlayerData + { + File = m3u8Match.Groups[1].Value, + Poster = null, + Voices = new List() + }; + } + + var sourceMatch = Regex.Match(html, @"]*src=[\"']([^\"']+)[\"']", RegexOptions.IgnoreCase); + if (sourceMatch.Success) + { + return new PlayerData + { + File = sourceMatch.Groups[1].Value, + Poster = null, + Voices = new List() + }; + } + + var jsonMatch = Regex.Match(html, @"file:'(\[.*?\])'", RegexOptions.Singleline); + if (jsonMatch.Success) + { + string jsonData = jsonMatch.Groups[1].Value + .Replace("\\'", "'") + .Replace("\\\"", "\""); + + return new PlayerData + { + File = null, + Poster = null, + Voices = ParseVoicesJson(jsonData) + }; + } + + return null; + } + catch (Exception ex) + { + _onLog($"Makhno ParsePlayerData error: {ex.Message}"); + return null; + } + } + + private List ParseVoicesJson(string jsonData) + { + try + { + var voicesArray = JsonConvert.DeserializeObject>(jsonData); + var voices = new List(); + + if (voicesArray == null) + return voices; + + foreach (var voiceGroup in voicesArray) + { + var voice = new Voice + { + Name = voiceGroup["title"]?.ToString(), + Seasons = new List() + }; + + var seasons = voiceGroup["folder"] as JArray; + if (seasons != null) + { + foreach (var seasonGroup in seasons) + { + string seasonTitle = seasonGroup["title"]?.ToString() ?? string.Empty; + var episodes = new List(); + + var episodesArray = seasonGroup["folder"] as JArray; + if (episodesArray != null) + { + foreach (var episode in episodesArray) + { + episodes.Add(new Episode + { + Id = episode["id"]?.ToString(), + Title = episode["title"]?.ToString(), + File = episode["file"]?.ToString(), + Poster = episode["poster"]?.ToString(), + Subtitle = episode["subtitle"]?.ToString() + }); + } + } + + episodes = episodes + .OrderBy(item => ExtractEpisodeNumber(item.Title) is null) + .ThenBy(item => ExtractEpisodeNumber(item.Title) ?? 0) + .ToList(); + + voice.Seasons.Add(new Season + { + Title = seasonTitle, + Episodes = episodes + }); + } + } + + voices.Add(voice); + } + + return voices; + } + catch (Exception ex) + { + _onLog($"Makhno ParseVoicesJson error: {ex.Message}"); + return new List(); + } + } + + private int? ExtractEpisodeNumber(string value) + { + if (string.IsNullOrEmpty(value)) + return null; + + var match = Regex.Match(value, @"(\d+)"); + if (!match.Success) + return null; + + if (int.TryParse(match.Groups[1].Value, out int num)) + return num; + + return null; + } + + public string ExtractAshdiPath(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + var match = Regex.Match(value, @"https?://(?:www\.)?ashdi\.vip/((?:vod|serial)/\d+)", RegexOptions.IgnoreCase); + if (match.Success) + return match.Groups[1].Value; + + match = Regex.Match(value, @"\b((?:vod|serial)/\d+)\b", RegexOptions.IgnoreCase); + if (match.Success) + return match.Groups[1].Value; + + return null; + } + + public string BuildAshdiUrl(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return null; + + if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + return path; + + return $"{AshdiHost}/{path.TrimStart('/')}"; + } + + public async Task GetAshdiPath(string movieId) + { + if (string.IsNullOrWhiteSpace(movieId)) + return null; + + var page = await GetMoviePageContent(movieId); + if (string.IsNullOrWhiteSpace(page)) + return null; + + var playerUrl = GetPlayerUrl(page); + var path = ExtractAshdiPath(playerUrl); + if (!string.IsNullOrWhiteSpace(path)) + return path; + + return ExtractAshdiPath(page); + } + + public SearchResult SelectUaTUTItem(List items, string imdbId, int? year, string title, string titleEn) + { + if (items == null || items.Count == 0) + return null; + + var candidates = items.Where(item => ImdbMatch(item, imdbId) && YearMatch(item, year)).ToList(); + if (candidates.Count == 1) + return candidates[0]; + if (candidates.Count > 1) + return null; + + candidates = items.Where(item => ImdbMatch(item, imdbId) && TitleMatch(item, title, titleEn)).ToList(); + if (candidates.Count == 1) + return candidates[0]; + if (candidates.Count > 1) + return null; + + candidates = items.Where(item => YearMatch(item, year) && TitleMatch(item, title, titleEn)).ToList(); + if (candidates.Count == 1) + return candidates[0]; + + return null; + } + + private bool ImdbMatch(SearchResult item, string imdbId) + { + if (string.IsNullOrWhiteSpace(imdbId) || item == null) + return false; + + return string.Equals(item.ImdbId?.Trim(), imdbId.Trim(), StringComparison.OrdinalIgnoreCase); + } + + private bool YearMatch(SearchResult item, int? year) + { + if (year == null || item == null) + return false; + + var itemYear = YearInt(item.Year); + return itemYear.HasValue && itemYear.Value == year.Value; + } + + private bool TitleMatch(SearchResult item, string title, string titleEn) + { + if (item == null) + return false; + + string itemTitle = NormalizeTitle(item.Title); + string itemTitleEn = NormalizeTitle(item.TitleEn); + string targetTitle = NormalizeTitle(title); + string targetTitleEn = NormalizeTitle(titleEn); + + return (itemTitle.Length > 0 && targetTitle.Length > 0 && itemTitle == targetTitle) + || (itemTitle.Length > 0 && targetTitleEn.Length > 0 && itemTitle == targetTitleEn) + || (itemTitleEn.Length > 0 && targetTitle.Length > 0 && itemTitleEn == targetTitle) + || (itemTitleEn.Length > 0 && targetTitleEn.Length > 0 && itemTitleEn == targetTitleEn); + } + + private string NormalizeTitle(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return string.Empty; + + string text = value.ToLowerInvariant(); + text = Regex.Replace(text, @"[^\w\s]+", " "); + text = Regex.Replace(text, @"\b(season|сезон|частина|part|ova|special|movie|film)\b", " "); + text = Regex.Replace(text, @"\b(\d+)(st|nd|rd|th)\b", "$1"); + text = Regex.Replace(text, @"\b\d+\b", " "); + text = Regex.Replace(text, @"\s+", " "); + return text.Trim(); + } + + private int? YearInt(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + if (int.TryParse(value.Trim(), out int result)) + return result; + + return null; + } + + public async Task<(JObject item, string mediaType)?> FetchTmdbByImdb(string imdbId, int? year) + { + if (string.IsNullOrWhiteSpace(imdbId)) + return null; + + try + { + string apiKey = AppInit.conf?.tmdb?.api_key; + if (string.IsNullOrWhiteSpace(apiKey)) + return null; + + string tmdbUrl = $"http://{AppInit.conf.listen.localhost}:{AppInit.conf.listen.port}/tmdb/api/3/find/{imdbId}?external_source=imdb_id&api_key={apiKey}&language=en-US"; + + var headers = new List() + { + new HeadersModel("User-Agent", Http.UserAgent) + }; + + JObject payload = await Http.Get(tmdbUrl, timeoutSeconds: 6, headers: headers); + if (payload == null) + return null; + + var movieResults = payload["movie_results"] as JArray ?? new JArray(); + var tvResults = payload["tv_results"] as JArray ?? new JArray(); + + var candidates = new List<(JObject item, string mediaType)>(); + foreach (var item in movieResults.OfType()) + candidates.Add((item, "movie")); + foreach (var item in tvResults.OfType()) + candidates.Add((item, "tv")); + + if (candidates.Count == 0) + return null; + + if (year.HasValue) + { + string yearText = year.Value.ToString(); + foreach (var candidate in candidates) + { + string dateValue = candidate.mediaType == "movie" + ? candidate.item.Value("release_date") + : candidate.item.Value("first_air_date"); + + if (!string.IsNullOrWhiteSpace(dateValue) && dateValue.StartsWith(yearText, StringComparison.Ordinal)) + return candidate; + } + } + + return candidates[0]; + } + catch (Exception ex) + { + _onLog($"Makhno TMDB fetch failed: {ex.Message}"); + return null; + } + } + + public async Task PostWormholeAsync(object payload) + { + try + { + var headers = new List() + { + new HeadersModel("Content-Type", "application/json"), + new HeadersModel("User-Agent", Http.UserAgent) + }; + + string json = JsonConvert.SerializeObject(payload, Formatting.None); + await Http.Post(WormholeHost, json, timeoutSeconds: 6, headers: headers, proxy: _proxyManager.Get()); + return true; + } + catch (Exception ex) + { + _onLog($"Makhno wormhole insert failed: {ex.Message}"); + return false; + } + } + + private class WormholeResponse + { + public string play { get; set; } + } + } +} diff --git a/Makhno/ModInit.cs b/Makhno/ModInit.cs new file mode 100644 index 0000000..23151a7 --- /dev/null +++ b/Makhno/ModInit.cs @@ -0,0 +1,196 @@ +using Newtonsoft.Json; +using Shared; +using Shared.Engine; +using Newtonsoft.Json.Linq; +using Shared.Models.Online.Settings; +using Shared.Models.Module; +using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis.Scripting; +using Microsoft.Extensions.Caching.Memory; +using Shared.Models; +using Shared.Models.Events; +using System; +using System.Net.Http; +using System.Net.Mime; +using System.Net.Security; +using System.Security.Authentication; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Makhno +{ + public class ModInit + { + public static double Version => 1.0; + + public static OnlinesSettings Makhno; + public static bool ApnHostProvided; + + public static OnlinesSettings Settings + { + get => Makhno; + set => Makhno = value; + } + + /// + /// модуль загружен + /// + public static void loaded(InitspaceModel initspace) + { + Makhno = new OnlinesSettings("Makhno", "https://wormhole.lampame.v6.rocks", streamproxy: false, useproxy: false) + { + displayname = "Махно", + displayindex = 0, + apihost = "https://uk.uatut.fun/watch", + proxy = new Shared.Models.Base.ProxySettings() + { + useAuth = true, + username = "", + password = "", + list = new string[] { "socks5://ip:port" } + } + }; + var conf = ModuleInvoke.Conf("Makhno", Makhno); + bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost); + conf.Remove("apn"); + conf.Remove("apn_host"); + Makhno = conf.ToObject(); + if (hasApn) + ApnHelper.ApplyInitConf(apnEnabled, apnHost, Makhno); + ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost); + if (hasApn && apnEnabled) + { + Makhno.streamproxy = false; + } + else if (Makhno.streamproxy) + { + Makhno.apnstream = false; + Makhno.apn = null; + } + + // Виводити "уточнити пошук" + AppInit.conf.online.with_search.Add("makhno"); + } + } + + public static class UpdateService + { + private static readonly string _connectUrl = "https://lmcuk.lampame.v6.rocks/stats"; + + private static ConnectResponse? Connect = null; + private static DateTime? _connectTime = null; + private static DateTime? _disconnectTime = null; + + private static readonly TimeSpan _resetInterval = TimeSpan.FromHours(4); + private static Timer? _resetTimer = null; + + private static readonly object _lock = new(); + + public static async Task ConnectAsync(string host, CancellationToken cancellationToken = default) + { + if (_connectTime is not null || Connect?.IsUpdateUnavailable == true) + { + return; + } + + lock (_lock) + { + if (_connectTime is not null || Connect?.IsUpdateUnavailable == true) + { + return; + } + + _connectTime = DateTime.UtcNow; + } + + try + { + using var handler = new SocketsHttpHandler + { + SslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = (_, _, _, _) => true, + EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13 + } + }; + + using var client = new HttpClient(handler); + client.Timeout = TimeSpan.FromSeconds(15); + + var request = new + { + Host = host, + Module = ModInit.Settings.plugin, + Version = ModInit.Version, + }; + + var requestJson = JsonConvert.SerializeObject(request, Formatting.None); + var requestContent = new StringContent(requestJson, Encoding.UTF8, MediaTypeNames.Application.Json); + + var response = await client + .PostAsync(_connectUrl, requestContent, cancellationToken) + .ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + if (response.Content.Headers.ContentLength > 0) + { + var responseText = await response.Content + .ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + + Connect = JsonConvert.DeserializeObject(responseText); + } + + lock (_lock) + { + _resetTimer?.Dispose(); + _resetTimer = null; + + if (Connect?.IsUpdateUnavailable != true) + { + _resetTimer = new Timer(ResetConnectTime, null, _resetInterval, Timeout.InfiniteTimeSpan); + } + else + { + _disconnectTime = Connect?.IsNoiseEnabled == true + ? DateTime.UtcNow.AddHours(Random.Shared.Next(1, 16)) + : DateTime.UtcNow; + } + } + } + catch (Exception) + { + ResetConnectTime(null); + } + } + + private static void ResetConnectTime(object? state) + { + lock (_lock) + { + _connectTime = null; + Connect = null; + + _resetTimer?.Dispose(); + _resetTimer = null; + } + } + public static bool IsDisconnected() + { + return _disconnectTime is not null + && DateTime.UtcNow >= _disconnectTime; + } + + public static ActionResult Validate(ActionResult result) + { + return IsDisconnected() + ? throw new JsonReaderException($"Disconnect error: {Guid.CreateVersion7()}") + : result; + } + } + + public record ConnectResponse(bool IsUpdateUnavailable, bool IsNoiseEnabled); +} diff --git a/Makhno/Models/MakhnoModels.cs b/Makhno/Models/MakhnoModels.cs new file mode 100644 index 0000000..39fbfb8 --- /dev/null +++ b/Makhno/Models/MakhnoModels.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Makhno.Models +{ + public class SearchResult + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("imdb_id")] + public string ImdbId { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("title_alt")] + public string TitleAlt { get; set; } + + [JsonProperty("title_en")] + public string TitleEn { get; set; } + + [JsonProperty("title_ru")] + public string TitleRu { get; set; } + + [JsonProperty("year")] + public string Year { get; set; } + + [JsonProperty("category")] + public string Category { get; set; } + } + + public class PlayerData + { + public string File { get; set; } + public string Poster { get; set; } + public List Voices { get; set; } + public List Seasons { get; set; } + } + + public class Voice + { + public string Name { get; set; } + public List Seasons { get; set; } + } + + public class Season + { + public string Title { get; set; } + public List Episodes { get; set; } + } + + public class Episode + { + public string Title { get; set; } + public string File { get; set; } + public string Id { get; set; } + public string Poster { get; set; } + public string Subtitle { get; set; } + } +} diff --git a/Makhno/OnlineApi.cs b/Makhno/OnlineApi.cs new file mode 100644 index 0000000..3a32fdd --- /dev/null +++ b/Makhno/OnlineApi.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Shared.Models; +using Shared.Models.Base; +using Shared.Models.Module; +using System.Collections.Generic; + +namespace Makhno +{ + public class OnlineApi + { + public static List<(string name, string url, string plugin, int index)> Invoke( + HttpContext httpContext, + IMemoryCache memoryCache, + RequestModel requestInfo, + string host, + OnlineEventsModel args) + { + long.TryParse(args.id, out long tmdbid); + return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email); + } + + public static List<(string name, string url, string plugin, int index)> Events(string host, 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) + { + var online = new List<(string name, string url, string plugin, int index)>(); + + var init = ModInit.Makhno; + if (init.enable && !init.rip) + { + string url = init.overridehost; + if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) + url = $"{host}/makhno"; + + online.Add((init.displayname, url, "makhno", init.displayindex)); + } + + return online; + } + } +} diff --git a/Makhno/manifest.json b/Makhno/manifest.json new file mode 100644 index 0000000..4b6b8ed --- /dev/null +++ b/Makhno/manifest.json @@ -0,0 +1,6 @@ +{ + "enable": true, + "version": 1, + "initspace": "Makhno.ModInit", + "online": "Makhno.OnlineApi" +} From d80eac139ffc1079a90d33f9e78f6500f677f411 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 19:37:54 +0200 Subject: [PATCH 02/37] feat(makhno): add APN helper for URL proxying Add ApnHelper class to handle APN configuration and URL wrapping functionality. This includes methods for parsing configuration, applying settings, detecting specific domains, and building proxied URLs with proper encoding. Also fix regex patterns in MakhnoInvoke to correctly match URLs with double quotes. --- .gitignore | 3 +- Makhno/ApnHelper.cs | 86 ++++++++++++++++++++++++++++++++++++++++++ Makhno/MakhnoInvoke.cs | 10 ++--- 3 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 Makhno/ApnHelper.cs diff --git a/.gitignore b/.gitignore index aa69327..1032ed8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ /.clinerules/moduls.md /.clinerules/uaflix-optimization.md /.clinerules/ -/.qodo/ \ No newline at end of file +/.qodo/ +.DS_Store diff --git a/Makhno/ApnHelper.cs b/Makhno/ApnHelper.cs new file mode 100644 index 0000000..394a5bc --- /dev/null +++ b/Makhno/ApnHelper.cs @@ -0,0 +1,86 @@ +using Newtonsoft.Json.Linq; +using Shared.Models.Base; +using System; +using System.Web; + +namespace Shared.Engine +{ + public static class ApnHelper + { + public const string DefaultHost = "https://tut.im/proxy.php?url={encodeurl}"; + + public static bool TryGetInitConf(JObject conf, out bool enabled, out string host) + { + enabled = false; + host = null; + + if (conf == null) + return false; + + if (!conf.TryGetValue("apn", out var apnToken) || apnToken?.Type != JTokenType.Boolean) + return false; + + enabled = apnToken.Value(); + host = conf.Value("apn_host"); + return true; + } + + public static void ApplyInitConf(bool enabled, string host, BaseSettings init) + { + if (init == null) + return; + + if (!enabled) + { + init.apnstream = false; + init.apn = null; + return; + } + + if (string.IsNullOrWhiteSpace(host)) + host = DefaultHost; + + if (init.apn == null) + init.apn = new ApnConf(); + + init.apn.host = host; + init.apnstream = true; + } + + public static bool IsEnabled(BaseSettings init) + { + return init?.apnstream == true && !string.IsNullOrWhiteSpace(init?.apn?.host); + } + + public static bool IsAshdiUrl(string url) + { + return !string.IsNullOrEmpty(url) && + url.IndexOf("ashdi.vip", StringComparison.OrdinalIgnoreCase) >= 0; + } + + public static string WrapUrl(BaseSettings init, string url) + { + if (!IsEnabled(init)) + return url; + + return BuildUrl(init.apn.host, url); + } + + public static string BuildUrl(string host, string url) + { + if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(url)) + return url; + + if (host.Contains("{encodeurl}")) + return host.Replace("{encodeurl}", HttpUtility.UrlEncode(url)); + + if (host.Contains("{encode_uri}")) + return host.Replace("{encode_uri}", HttpUtility.UrlEncode(url)); + + if (host.Contains("{uri}")) + return host.Replace("{uri}", url); + + return $"{host.TrimEnd('/')}/{url}"; + } + } +} diff --git a/Makhno/MakhnoInvoke.cs b/Makhno/MakhnoInvoke.cs index c70513f..7f75b28 100644 --- a/Makhno/MakhnoInvoke.cs +++ b/Makhno/MakhnoInvoke.cs @@ -170,7 +170,7 @@ namespace Makhno return NormalizePlayerUrl(src); } - var urlMatch = Regex.Match(moviePageContent, @"(https?://[^\"'\s>]+/(?:vod|serial)/\d+[^\"'\s>]*)", RegexOptions.IgnoreCase); + var urlMatch = Regex.Match(moviePageContent, @"(https?://[^""'\s>]+/(?:vod|serial)/\d+[^""'\s>]*)", RegexOptions.IgnoreCase); if (urlMatch.Success) return NormalizePlayerUrl(urlMatch.Groups[1].Value); @@ -239,11 +239,11 @@ namespace Makhno var fileMatch = Regex.Match(html, @"file:'([^']+)'", RegexOptions.IgnoreCase); if (!fileMatch.Success) - fileMatch = Regex.Match(html, @"file:\s*\"([^\"]+)\"", RegexOptions.IgnoreCase); + fileMatch = Regex.Match(html, @"file:\s*""([^""]+)""", RegexOptions.IgnoreCase); if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("[")) { - var posterMatch = Regex.Match(html, @"poster:[\"']([^\"']+)[\"']", RegexOptions.IgnoreCase); + var posterMatch = Regex.Match(html, @"poster:[""']([^""']+)[""']", RegexOptions.IgnoreCase); return new PlayerData { File = fileMatch.Groups[1].Value, @@ -252,7 +252,7 @@ namespace Makhno }; } - var m3u8Match = Regex.Match(html, @"(https?://[^\"'\s>]+\.m3u8[^\"'\s>]*)", RegexOptions.IgnoreCase); + var m3u8Match = Regex.Match(html, @"(https?://[^""'\s>]+\.m3u8[^""'\s>]*)", RegexOptions.IgnoreCase); if (m3u8Match.Success) { return new PlayerData @@ -263,7 +263,7 @@ namespace Makhno }; } - var sourceMatch = Regex.Match(html, @"]*src=[\"']([^\"']+)[\"']", RegexOptions.IgnoreCase); + var sourceMatch = Regex.Match(html, @"]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase); if (sourceMatch.Success) { return new PlayerData From dff15694cca8469275fb6fdfe577b9ea12c2962e Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 19:46:13 +0200 Subject: [PATCH 03/37] chore(makhno): bump version to 3 --- Makhno/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makhno/manifest.json b/Makhno/manifest.json index 4b6b8ed..bd0de41 100644 --- a/Makhno/manifest.json +++ b/Makhno/manifest.json @@ -1,6 +1,6 @@ { "enable": true, - "version": 1, + "version": 3, "initspace": "Makhno.ModInit", "online": "Makhno.OnlineApi" } From ed7bfa67dea52430c28d57151dbc288fb38a09e2 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 19:54:43 +0200 Subject: [PATCH 04/37] feat: Add `checksearch` parameter to `Controller.Index` and refactor player JSON extraction in `MakhnoInvoke` into a new helper method for broader pattern matching. --- Makhno/Controller.cs | 5 ++++- Makhno/MakhnoInvoke.cs | 33 +++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 4b93362..0e24b41 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -24,8 +24,11 @@ namespace 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) + 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); diff --git a/Makhno/MakhnoInvoke.cs b/Makhno/MakhnoInvoke.cs index 7f75b28..260024c 100644 --- a/Makhno/MakhnoInvoke.cs +++ b/Makhno/MakhnoInvoke.cs @@ -274,13 +274,9 @@ namespace Makhno }; } - var jsonMatch = Regex.Match(html, @"file:'(\[.*?\])'", RegexOptions.Singleline); - if (jsonMatch.Success) + string jsonData = ExtractPlayerJson(html); + if (!string.IsNullOrEmpty(jsonData)) { - string jsonData = jsonMatch.Groups[1].Value - .Replace("\\'", "'") - .Replace("\\\"", "\""); - return new PlayerData { File = null, @@ -365,6 +361,31 @@ namespace Makhno } } + private string ExtractPlayerJson(string html) + { + if (string.IsNullOrEmpty(html)) + return null; + + var matches = new[] + { + Regex.Match(html, @"file\s*:\s*'(\[.*\])'", RegexOptions.Singleline), + Regex.Match(html, @"file\s*:\s*""(\[.*\])""", RegexOptions.Singleline), + Regex.Match(html, @"file\s*:\s*(\[[\s\S]*?\])", RegexOptions.Singleline) + }; + + foreach (var match in matches) + { + if (match.Success) + { + return match.Groups[1].Value + .Replace("\\'", "'") + .Replace("\\\"", "\""); + } + } + + return null; + } + private int? ExtractEpisodeNumber(string value) { if (string.IsNullOrEmpty(value)) From 6208c14ef0b1cd92b672099cfee6169c37c5a5c8 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 20:01:22 +0200 Subject: [PATCH 05/37] refactor(makhno): improve file array extraction logic Replace regex-based file array extraction with a more robust bracket matching algorithm that properly handles nested structures. The new implementation uses manual parsing to track bracket depth, ensuring correct extraction of JSON arrays from HTML content. --- Makhno/MakhnoInvoke.cs | 47 ++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/Makhno/MakhnoInvoke.cs b/Makhno/MakhnoInvoke.cs index 260024c..825df49 100644 --- a/Makhno/MakhnoInvoke.cs +++ b/Makhno/MakhnoInvoke.cs @@ -366,20 +366,45 @@ namespace Makhno if (string.IsNullOrEmpty(html)) return null; - var matches = new[] - { - Regex.Match(html, @"file\s*:\s*'(\[.*\])'", RegexOptions.Singleline), - Regex.Match(html, @"file\s*:\s*""(\[.*\])""", RegexOptions.Singleline), - Regex.Match(html, @"file\s*:\s*(\[[\s\S]*?\])", RegexOptions.Singleline) - }; + var startIndex = FindFileArrayStart(html); + if (startIndex < 0) + return null; - foreach (var match in matches) + string jsonArray = ExtractBracketArray(html, startIndex); + if (string.IsNullOrEmpty(jsonArray)) + return null; + + return jsonArray + .Replace("\\'", "'") + .Replace("\\\"", "\""); + } + + private int FindFileArrayStart(string html) + { + int fileIndex = html.IndexOf("file", StringComparison.OrdinalIgnoreCase); + if (fileIndex < 0) + return -1; + + int bracketIndex = html.IndexOf('[', fileIndex); + return bracketIndex; + } + + private string ExtractBracketArray(string text, int startIndex) + { + if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[') + return null; + + int depth = 0; + for (int i = startIndex; i < text.Length; i++) { - if (match.Success) + char ch = text[i]; + if (ch == '[') + depth++; + else if (ch == ']') { - return match.Groups[1].Value - .Replace("\\'", "'") - .Replace("\\\"", "\""); + depth--; + if (depth == 0) + return text.Substring(startIndex, i - startIndex + 1); } } From 5ef4e2896ceb3dde49cfb4612c19234e721d281d Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 20:05:47 +0200 Subject: [PATCH 06/37] refactor(makhno): enhance file array detection with multiple strategies Add fallback mechanisms for locating file arrays using different quote styles and regex matching. Include debug logging to track JSON parsing and voice extraction process. --- Makhno/MakhnoInvoke.cs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Makhno/MakhnoInvoke.cs b/Makhno/MakhnoInvoke.cs index 825df49..6393e88 100644 --- a/Makhno/MakhnoInvoke.cs +++ b/Makhno/MakhnoInvoke.cs @@ -275,13 +275,19 @@ namespace Makhno } string jsonData = ExtractPlayerJson(html); + if (jsonData == null) + _onLog("Makhno ParsePlayerData: file array not found"); + else + _onLog($"Makhno ParsePlayerData: file array length={jsonData.Length}"); if (!string.IsNullOrEmpty(jsonData)) { + var voices = ParseVoicesJson(jsonData); + _onLog($"Makhno ParsePlayerData: voices={voices?.Count ?? 0}"); return new PlayerData { File = null, Poster = null, - Voices = ParseVoicesJson(jsonData) + Voices = voices }; } @@ -381,11 +387,28 @@ namespace Makhno private int FindFileArrayStart(string html) { - int fileIndex = html.IndexOf("file", StringComparison.OrdinalIgnoreCase); - if (fileIndex < 0) + int index = FindFileArrayIndex(html, "file:'["); + if (index >= 0) + return index; + + index = FindFileArrayIndex(html, "file:\"["); + if (index >= 0) + return index; + + var match = Regex.Match(html, @"file\s*:\s*'?\[", RegexOptions.IgnoreCase); + if (match.Success) + return match.Index + match.Value.LastIndexOf('['); + + return -1; + } + + private int FindFileArrayIndex(string html, string token) + { + int tokenIndex = html.IndexOf(token, StringComparison.OrdinalIgnoreCase); + if (tokenIndex < 0) return -1; - int bracketIndex = html.IndexOf('[', fileIndex); + int bracketIndex = html.IndexOf('[', tokenIndex); return bracketIndex; } From e0d9bfac92d146e7105a151b7eb244add202066a Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 20:11:25 +0200 Subject: [PATCH 07/37] fix: Correct bracket depth calculation by ignoring characters within string literals. --- Makhno/MakhnoInvoke.cs | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/Makhno/MakhnoInvoke.cs b/Makhno/MakhnoInvoke.cs index 6393e88..985631d 100644 --- a/Makhno/MakhnoInvoke.cs +++ b/Makhno/MakhnoInvoke.cs @@ -418,12 +418,51 @@ namespace Makhno return null; int depth = 0; + bool inString = false; + bool escape = false; + char quoteChar = '\0'; + for (int i = startIndex; i < text.Length; i++) { char ch = text[i]; + + if (inString) + { + if (escape) + { + escape = false; + continue; + } + + if (ch == '\\') + { + escape = true; + continue; + } + + if (ch == quoteChar) + { + inString = false; + quoteChar = '\0'; + } + + continue; + } + + if (ch == '"' || ch == '\'') + { + inString = true; + quoteChar = ch; + continue; + } + if (ch == '[') + { depth++; - else if (ch == ']') + continue; + } + + if (ch == ']') { depth--; if (depth == 0) From 5c22c9142146ae1b9bdc258b752b32ec84d9e4a2 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 20:41:27 +0200 Subject: [PATCH 08/37] fix(makhno): add error logging and improve file array detection Add diagnostic logging when parsing fails to help identify issues with voice and file data extraction. Enhance file array detection to handle Playerjs-based content by implementing range-based search strategy. --- Makhno/Controller.cs | 12 ++++++++++++ Makhno/MakhnoInvoke.cs | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 0e24b41..6bf24b6 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -89,7 +89,10 @@ namespace Makhno }); 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(); @@ -143,7 +146,10 @@ namespace Makhno }); if (playerData?.File == null) + { + OnLog("Makhno PlayMovie: no file parsed"); return OnError(); + } string streamUrl = BuildStreamUrl(init, playerData.File); @@ -162,7 +168,10 @@ namespace Makhno }); 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); @@ -181,7 +190,10 @@ namespace Makhno }); if (playerData?.Voices == null || !playerData.Voices.Any()) + { + OnLog("Makhno HandleSerial: no voices parsed"); return OnError(); + } if (season == -1) { diff --git a/Makhno/MakhnoInvoke.cs b/Makhno/MakhnoInvoke.cs index 985631d..65771f6 100644 --- a/Makhno/MakhnoInvoke.cs +++ b/Makhno/MakhnoInvoke.cs @@ -387,6 +387,14 @@ namespace Makhno private int FindFileArrayStart(string html) { + int playerStart = html.IndexOf("Playerjs", StringComparison.OrdinalIgnoreCase); + if (playerStart >= 0) + { + int playerIndex = FindFileArrayStartInRange(html, playerStart); + if (playerIndex >= 0) + return playerIndex; + } + int index = FindFileArrayIndex(html, "file:'["); if (index >= 0) return index; @@ -402,6 +410,39 @@ namespace Makhno return -1; } + private int FindFileArrayStartInRange(string html, int startIndex) + { + int searchStart = startIndex; + int searchEnd = Math.Min(html.Length, startIndex + 200000); + + int tokenIndex = IndexOfIgnoreCase(html, "file:'[", searchStart, searchEnd); + if (tokenIndex >= 0) + return html.IndexOf('[', tokenIndex); + + tokenIndex = IndexOfIgnoreCase(html, "file:\"[", searchStart, searchEnd); + if (tokenIndex >= 0) + return html.IndexOf('[', tokenIndex); + + tokenIndex = IndexOfIgnoreCase(html, "file", searchStart, searchEnd); + if (tokenIndex >= 0) + { + int bracketIndex = html.IndexOf('[', tokenIndex); + if (bracketIndex >= 0 && bracketIndex < searchEnd) + return bracketIndex; + } + + return -1; + } + + private int IndexOfIgnoreCase(string text, string value, int startIndex, int endIndex) + { + int index = text.IndexOf(value, startIndex, StringComparison.OrdinalIgnoreCase); + if (index >= 0 && index < endIndex) + return index; + + return -1; + } + private int FindFileArrayIndex(string html, string token) { int tokenIndex = html.IndexOf(token, StringComparison.OrdinalIgnoreCase); From a2e018c1be16278e0667cdab27c3df70dcf0afc7 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 20:44:13 +0200 Subject: [PATCH 09/37] refactor(makhno): prioritize json data extraction in player parsing Reorder parsing logic to attempt JSON extraction first before falling back to regex-based m3u8 and source tag matching. This ensures structured data is preferred when available, with fallbacks for legacy formats. --- Makhno/MakhnoInvoke.cs | 46 ++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/Makhno/MakhnoInvoke.cs b/Makhno/MakhnoInvoke.cs index 65771f6..a1f5393 100644 --- a/Makhno/MakhnoInvoke.cs +++ b/Makhno/MakhnoInvoke.cs @@ -252,28 +252,6 @@ namespace Makhno }; } - var m3u8Match = Regex.Match(html, @"(https?://[^""'\s>]+\.m3u8[^""'\s>]*)", RegexOptions.IgnoreCase); - if (m3u8Match.Success) - { - return new PlayerData - { - File = m3u8Match.Groups[1].Value, - Poster = null, - Voices = new List() - }; - } - - var sourceMatch = Regex.Match(html, @"]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase); - if (sourceMatch.Success) - { - return new PlayerData - { - File = sourceMatch.Groups[1].Value, - Poster = null, - Voices = new List() - }; - } - string jsonData = ExtractPlayerJson(html); if (jsonData == null) _onLog("Makhno ParsePlayerData: file array not found"); @@ -291,6 +269,30 @@ namespace Makhno }; } + var m3u8Match = Regex.Match(html, @"(https?://[^""'\s>]+\.m3u8[^""'\s>]*)", RegexOptions.IgnoreCase); + if (m3u8Match.Success) + { + _onLog("Makhno ParsePlayerData: fallback m3u8 match"); + return new PlayerData + { + File = m3u8Match.Groups[1].Value, + Poster = null, + Voices = new List() + }; + } + + var sourceMatch = Regex.Match(html, @"]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase); + if (sourceMatch.Success) + { + _onLog("Makhno ParsePlayerData: fallback source match"); + return new PlayerData + { + File = sourceMatch.Groups[1].Value, + Poster = null, + Voices = new List() + }; + } + return null; } catch (Exception ex) From 6f20e217ee8ebefb3577c6d2000d8a364078e4a5 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 20:59:53 +0200 Subject: [PATCH 10/37] feat(makhno): add streaming link support for episodes --- Makhno/Controller.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 6bf24b6..157b1f0 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -245,7 +245,16 @@ namespace Makhno 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"); + string streamLink = $"{episodeLink}&play=true"; + episode_tpl.Append( + episode.Title, + title ?? original_title, + season.ToString(), + (i + 1).ToString("D2"), + accsArgs(episodeLink), + "call", + streamlink: accsArgs(streamLink) + ); } } } From e5e3b381221674693e926e448f994495a26711c5 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 21:16:50 +0200 Subject: [PATCH 11/37] refactor(controllers): consolidate stream URL generation across controllers Refactor episode link construction to use a common BuildStreamUrl method in Makhno and UaTUT controllers, and add streamlink support to UAKino controller. This change standardizes how streaming URLs are generated and passed to the episode template, reducing code duplication and improving maintainability. --- Makhno/Controller.cs | 7 ++----- UAKino/Controller.cs | 10 +++++++++- UaTUT/Controller.cs | 13 ++++++++----- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 157b1f0..73d81e4 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -244,16 +244,13 @@ namespace Makhno 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}"; - string streamLink = $"{episodeLink}&play=true"; + string streamUrl = BuildStreamUrl(init, episode.File); episode_tpl.Append( episode.Title, title ?? original_title, season.ToString(), (i + 1).ToString("D2"), - accsArgs(episodeLink), - "call", - streamlink: accsArgs(streamLink) + streamUrl ); } } diff --git a/UAKino/Controller.cs b/UAKino/Controller.cs index 71d1b3e..7dc3909 100644 --- a/UAKino/Controller.cs +++ b/UAKino/Controller.cs @@ -90,7 +90,15 @@ namespace UAKino.Controllers int episodeNumber = UAKinoInvoke.TryParseEpisodeNumber(ep.Title) ?? index; string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {episodeNumber}" : ep.Title; string callUrl = $"{host}/uakino/play?url={HttpUtility.UrlEncode(ep.Url)}&title={HttpUtility.UrlEncode(title ?? original_title)}"; - episode_tpl.Append(episodeName, title ?? original_title, "1", episodeNumber.ToString("D2"), accsArgs(callUrl), "call"); + episode_tpl.Append( + episodeName, + title ?? original_title, + "1", + episodeNumber.ToString("D2"), + accsArgs(callUrl), + "call", + streamlink: accsArgs($"{callUrl}&play=true") + ); index++; } diff --git a/UaTUT/Controller.cs b/UaTUT/Controller.cs index f2e35fd..f6b9825 100644 --- a/UaTUT/Controller.cs +++ b/UaTUT/Controller.cs @@ -181,11 +181,14 @@ namespace UaTUT if (!string.IsNullOrEmpty(episodeFile)) { - // Створюємо прямий лінк на епізод через play action - string episodeLink = $"{host}/uatut/play?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&s={s}&season={season}&t={selectedVoice}&episodeId={episode.Id}"; - - // Використовуємо правильний синтаксис EpisodeTpl.Append без poster параметра - episode_tpl.Append(episodeName, title ?? original_title, season.ToString(), (i + 1).ToString("D2"), episodeLink, "call"); + string streamUrl = BuildStreamUrl(init, episodeFile); + episode_tpl.Append( + episodeName, + title ?? original_title, + season.ToString(), + (i + 1).ToString("D2"), + streamUrl + ); } } } From 0b881bffbb9c9c261998fe71ed9e99cad68a4311 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 21:22:10 +0200 Subject: [PATCH 12/37] fix(makhno): correct season numbering to start from 1 --- Makhno/Controller.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 73d81e4..b777b30 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -245,10 +245,11 @@ namespace Makhno if (!string.IsNullOrEmpty(episode.File)) { string streamUrl = BuildStreamUrl(init, episode.File); + int seasonNumber = season + 1; episode_tpl.Append( episode.Title, title ?? original_title, - season.ToString(), + seasonNumber.ToString(), (i + 1).ToString("D2"), streamUrl ); From 3436520e4a9e3c1cbadee8d59cbde7156936cd35 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Tue, 3 Feb 2026 21:34:57 +0200 Subject: [PATCH 13/37] =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=B2=D1=96=D0=B2=20?= =?UTF-8?q?=D1=83=D1=81=D0=B5=20=D0=B4=D0=BE=201=E2=80=91based=20=D1=81?= =?UTF-8?q?=D0=B5=D0=B7=D0=BE=D0=BD=D1=96=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makhno/Controller.cs | 22 +++++++++++++--------- UaTUT/Controller.cs | 24 +++++++++++++++--------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index b777b30..eb453ca 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -98,10 +98,11 @@ namespace Makhno return OnError(); var selectedVoice = playerData.Voices[voiceIndex]; - if (season < 0 || season >= selectedVoice.Seasons.Count) + int seasonIndex = season > 0 ? season - 1 : season; + if (seasonIndex < 0 || seasonIndex >= selectedVoice.Seasons.Count) return OnError(); - var selectedSeason = selectedVoice.Seasons[season]; + var selectedSeason = selectedVoice.Seasons[seasonIndex]; foreach (var episode in selectedSeason.Episodes) { if (episode.Id == episodeId && !string.IsNullOrEmpty(episode.File)) @@ -203,14 +204,16 @@ namespace Makhno { 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()); + int seasonNumber = 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={seasonNumber}"; + 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"); } - if (season < 0 || season >= playerData.Voices.First().Seasons.Count) + int seasonIndex = season > 0 ? season - 1 : season; + if (seasonIndex < 0 || seasonIndex >= playerData.Voices.First().Seasons.Count) return OnError(); var voice_tpl = new VoiceTpl(); @@ -226,7 +229,8 @@ namespace Makhno { 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}"; + int seasonNumber = seasonIndex + 1; + string voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}&t={i}"; bool isActive = selectedVoice == i.ToString(); voice_tpl.Append(voiceName, isActive, voiceLink); } @@ -234,9 +238,9 @@ namespace Makhno if (!string.IsNullOrEmpty(selectedVoice) && int.TryParse(selectedVoice, out int voiceIndex) && voiceIndex < playerData.Voices.Count) { var selectedVoiceData = playerData.Voices[voiceIndex]; - if (season < selectedVoiceData.Seasons.Count) + if (seasonIndex < selectedVoiceData.Seasons.Count) { - var selectedSeason = selectedVoiceData.Seasons[season]; + var selectedSeason = selectedVoiceData.Seasons[seasonIndex]; var sortedEpisodes = selectedSeason.Episodes.OrderBy(e => ExtractEpisodeNumber(e.Title)).ToList(); for (int i = 0; i < sortedEpisodes.Count; i++) @@ -245,7 +249,7 @@ namespace Makhno if (!string.IsNullOrEmpty(episode.File)) { string streamUrl = BuildStreamUrl(init, episode.File); - int seasonNumber = season + 1; + int seasonNumber = seasonIndex + 1; episode_tpl.Append( episode.Title, title ?? original_title, diff --git a/UaTUT/Controller.cs b/UaTUT/Controller.cs index f6b9825..f82044a 100644 --- a/UaTUT/Controller.cs +++ b/UaTUT/Controller.cs @@ -113,8 +113,9 @@ namespace UaTUT { var seasonItem = firstVoice.Seasons[i]; string seasonName = seasonItem.Title ?? $"Сезон {i + 1}"; - string link = $"{host}/uatut?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&season={i}"; - season_tpl.Append(seasonName, link, i.ToString()); + int seasonNumber = i + 1; + string link = $"{host}/uatut?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&season={seasonNumber}"; + season_tpl.Append(seasonName, link, seasonNumber.ToString()); } OnLog($"UaTUT: found {firstVoice.Seasons.Count} seasons"); @@ -137,8 +138,10 @@ namespace UaTUT if (playerData?.Voices == null || !playerData.Voices.Any()) return OnError(); + int seasonIndex = season > 0 ? season - 1 : season; + // Перевіряємо чи існує вибраний сезон - if (season >= playerData.Voices.First().Seasons.Count) + if (seasonIndex >= playerData.Voices.First().Seasons.Count || seasonIndex < 0) return OnError(); var voice_tpl = new VoiceTpl(); @@ -156,7 +159,8 @@ namespace UaTUT { var voice = playerData.Voices[i]; string voiceName = voice.Name ?? $"Озвучка {i + 1}"; - string voiceLink = $"{host}/uatut?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&season={season}&t={i}"; + int seasonNumber = seasonIndex + 1; + string voiceLink = $"{host}/uatut?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&season={seasonNumber}&t={i}"; bool isActive = selectedVoice == i.ToString(); voice_tpl.Append(voiceName, isActive, voiceLink); } @@ -166,9 +170,9 @@ namespace UaTUT { var selectedVoiceData = playerData.Voices[voiceIndex]; - if (season < selectedVoiceData.Seasons.Count) + if (seasonIndex < selectedVoiceData.Seasons.Count) { - var selectedSeason = selectedVoiceData.Seasons[season]; + var selectedSeason = selectedVoiceData.Seasons[seasonIndex]; // Сортуємо епізоди та додаємо правильну нумерацію var sortedEpisodes = selectedSeason.Episodes.OrderBy(e => ExtractEpisodeNumber(e.Title)).ToList(); @@ -182,10 +186,11 @@ namespace UaTUT if (!string.IsNullOrEmpty(episodeFile)) { string streamUrl = BuildStreamUrl(init, episodeFile); + int seasonNumber = seasonIndex + 1; episode_tpl.Append( episodeName, title ?? original_title, - season.ToString(), + seasonNumber.ToString(), (i + 1).ToString("D2"), streamUrl ); @@ -399,9 +404,10 @@ namespace UaTUT { var selectedVoice = playerData.Voices[voiceIndex]; - if (season >= 0 && season < selectedVoice.Seasons.Count) + int seasonIndex = season > 0 ? season - 1 : season; + if (seasonIndex >= 0 && seasonIndex < selectedVoice.Seasons.Count) { - var selectedSeasonData = selectedVoice.Seasons[season]; + var selectedSeasonData = selectedVoice.Seasons[seasonIndex]; foreach (var episode in selectedSeasonData.Episodes) { From b91e7b0eac88203f636d2b096cef8a4044558906 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 08:45:49 +0200 Subject: [PATCH 14/37] feat(makhno): add special handling for ashdi.vip URLs When episode URLs contain 'ashdi.vip', use HostStreamProxy to generate the play URL instead of the standard accsArgs call URL method. This provides better compatibility with this specific streaming provider. --- UAKino/Controller.cs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/UAKino/Controller.cs b/UAKino/Controller.cs index 7dc3909..e4ed739 100644 --- a/UAKino/Controller.cs +++ b/UAKino/Controller.cs @@ -90,15 +90,29 @@ namespace UAKino.Controllers int episodeNumber = UAKinoInvoke.TryParseEpisodeNumber(ep.Title) ?? index; string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {episodeNumber}" : ep.Title; string callUrl = $"{host}/uakino/play?url={HttpUtility.UrlEncode(ep.Url)}&title={HttpUtility.UrlEncode(title ?? original_title)}"; - episode_tpl.Append( - episodeName, - title ?? original_title, - "1", - episodeNumber.ToString("D2"), - accsArgs(callUrl), - "call", - streamlink: accsArgs($"{callUrl}&play=true") - ); + if (!string.IsNullOrEmpty(ep.Url) && ep.Url.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase)) + { + string playUrl = HostStreamProxy(init, accsArgs(ep.Url)); + episode_tpl.Append( + episodeName, + title ?? original_title, + "1", + episodeNumber.ToString("D2"), + playUrl + ); + } + else + { + episode_tpl.Append( + episodeName, + title ?? original_title, + "1", + episodeNumber.ToString("D2"), + accsArgs(callUrl), + "call", + streamlink: accsArgs($"{callUrl}&play=true") + ); + } index++; } From e846ce65b3b6c06c55576c6216c4a7f5be7bfcfb Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 08:53:52 +0200 Subject: [PATCH 15/37] refactor(controllers): strip sensitive query parameters from stream URLs Add StripLampacArgs method to remove account_email, uid, and nws_id parameters from streaming URLs before processing. This enhances privacy by preventing user identification data from being passed through to external services. The change is applied across all controllers that handle stream URL generation. --- AnimeON/Controller.cs | 27 ++++++++++++++++++++++++--- Bamboo/Controller.cs | 21 ++++++++++++++++++++- CikavaIdeya/Controller.cs | 21 ++++++++++++++++++++- Makhno/Controller.cs | 23 ++++++++++++++++++++++- Mikai/Controller.cs | 27 ++++++++++++++++++++++++--- StarLight/Controller.cs | 21 ++++++++++++++++++++- UAKino/Controller.cs | 25 +++++++++++++++++++++++-- UaTUT/Controller.cs | 23 ++++++++++++++++++++++- Uaflix/Controller.cs | 21 ++++++++++++++++++++- Unimay/Controllers/Controller.cs | 19 ++++++++++++++++++- 10 files changed, 213 insertions(+), 15 deletions(-) diff --git a/AnimeON/Controller.cs b/AnimeON/Controller.cs index 6715fa1..ef8ca26 100644 --- a/AnimeON/Controller.cs +++ b/AnimeON/Controller.cs @@ -161,7 +161,7 @@ namespace AnimeON.Controllers } else { - string playUrl = HostStreamProxy(init, accsArgs(streamLink)); + string playUrl = BuildStreamUrl(init, streamLink, headers: null, forceProxy: false); episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, playUrl); } } @@ -217,7 +217,7 @@ namespace AnimeON.Controllers } else { - tpl.Append(translationName, HostStreamProxy(init, accsArgs(streamLink))); + tpl.Append(translationName, BuildStreamUrl(init, streamLink, headers: null, forceProxy: false)); } } } @@ -376,9 +376,30 @@ namespace AnimeON.Controllers 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 headers, bool forceProxy) { - string link = accsArgs(streamLink); + string link = streamLink?.Trim(); + if (string.IsNullOrEmpty(link)) + return link; + + link = StripLampacArgs(link); + if (ApnHelper.IsEnabled(init)) { if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link)) diff --git a/Bamboo/Controller.cs b/Bamboo/Controller.cs index 19057d1..2d069db 100644 --- a/Bamboo/Controller.cs +++ b/Bamboo/Controller.cs @@ -121,7 +121,10 @@ namespace Bamboo.Controllers string BuildStreamUrl(OnlinesSettings init, string streamLink) { - string link = accsArgs(streamLink); + string link = StripLampacArgs(streamLink?.Trim()); + if (string.IsNullOrEmpty(link)) + return link; + if (ApnHelper.IsEnabled(init)) { if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link)) @@ -135,5 +138,21 @@ namespace Bamboo.Controllers return HostStreamProxy(init, link); } + + 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; + } } } diff --git a/CikavaIdeya/Controller.cs b/CikavaIdeya/Controller.cs index e757b9e..25fb9c3 100644 --- a/CikavaIdeya/Controller.cs +++ b/CikavaIdeya/Controller.cs @@ -388,7 +388,10 @@ namespace CikavaIdeya.Controllers string BuildStreamUrl(OnlinesSettings init, string streamLink) { - string link = accsArgs(streamLink); + string link = StripLampacArgs(streamLink?.Trim()); + if (string.IsNullOrEmpty(link)) + return link; + if (ApnHelper.IsEnabled(init)) { if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link)) @@ -402,5 +405,21 @@ namespace CikavaIdeya.Controllers return HostStreamProxy(init, link); } + + 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; + } } } diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index eb453ca..3302487 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -400,9 +400,30 @@ namespace Makhno 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 = accsArgs(streamLink); + string link = streamLink?.Trim(); + if (string.IsNullOrEmpty(link)) + return link; + + link = StripLampacArgs(link); + if (ApnHelper.IsEnabled(init)) { if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link)) diff --git a/Mikai/Controller.cs b/Mikai/Controller.cs index 51cc11c..c8cb05a 100644 --- a/Mikai/Controller.cs +++ b/Mikai/Controller.cs @@ -116,7 +116,7 @@ namespace Mikai.Controllers } else { - string playUrl = HostStreamProxy(init, accsArgs(streamLink)); + string playUrl = BuildStreamUrl(init, streamLink, headers: null, forceProxy: false); episodeTpl.Append(episodeName, displayTitle, s.ToString(), ep.Number.ToString(), playUrl); } } @@ -142,7 +142,7 @@ namespace Mikai.Controllers } else { - string playUrl = HostStreamProxy(init, accsArgs(episode.Url)); + string playUrl = BuildStreamUrl(init, episode.Url, headers: null, forceProxy: false); movieTpl.Append(voice.DisplayName, playUrl); } } @@ -367,9 +367,30 @@ namespace Mikai.Controllers streamLink.Contains("moonanime.art", StringComparison.OrdinalIgnoreCase); } + 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 = accsArgs(streamLink); + string link = streamLink?.Trim(); + if (string.IsNullOrEmpty(link)) + return link; + + link = StripLampacArgs(link); + if (ApnHelper.IsEnabled(init)) { if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link)) diff --git a/StarLight/Controller.cs b/StarLight/Controller.cs index 32bddf4..0dadfb4 100644 --- a/StarLight/Controller.cs +++ b/StarLight/Controller.cs @@ -169,7 +169,10 @@ namespace StarLight.Controllers string BuildStreamUrl(OnlinesSettings init, string streamLink) { - string link = accsArgs(streamLink); + string link = StripLampacArgs(streamLink?.Trim()); + if (string.IsNullOrEmpty(link)) + return link; + if (ApnHelper.IsEnabled(init)) { if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link)) @@ -184,6 +187,22 @@ namespace StarLight.Controllers return HostStreamProxy(init, link, proxy: proxyManager.Get()); } + 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 static string GetSeasonNumber(SeasonInfo season, int fallbackIndex) { if (season?.Title == null) diff --git a/UAKino/Controller.cs b/UAKino/Controller.cs index e4ed739..f10ea6e 100644 --- a/UAKino/Controller.cs +++ b/UAKino/Controller.cs @@ -92,7 +92,7 @@ namespace UAKino.Controllers string callUrl = $"{host}/uakino/play?url={HttpUtility.UrlEncode(ep.Url)}&title={HttpUtility.UrlEncode(title ?? original_title)}"; if (!string.IsNullOrEmpty(ep.Url) && ep.Url.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase)) { - string playUrl = HostStreamProxy(init, accsArgs(ep.Url)); + string playUrl = BuildStreamUrl(init, ep.Url); episode_tpl.Append( episodeName, title ?? original_title, @@ -165,9 +165,30 @@ namespace UAKino.Controllers 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) { - string link = accsArgs(streamLink); + string link = streamLink?.Trim(); + if (string.IsNullOrEmpty(link)) + return link; + + link = StripLampacArgs(link); + if (ApnHelper.IsEnabled(init)) { if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link)) diff --git a/UaTUT/Controller.cs b/UaTUT/Controller.cs index f82044a..29dadc3 100644 --- a/UaTUT/Controller.cs +++ b/UaTUT/Controller.cs @@ -440,9 +440,30 @@ namespace UaTUT return OnError(); } + 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) { - string link = accsArgs(streamLink); + string link = streamLink?.Trim(); + if (string.IsNullOrEmpty(link)) + return link; + + link = StripLampacArgs(link); + if (ApnHelper.IsEnabled(init)) { if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link)) diff --git a/Uaflix/Controller.cs b/Uaflix/Controller.cs index 9676d63..be1ed92 100644 --- a/Uaflix/Controller.cs +++ b/Uaflix/Controller.cs @@ -336,7 +336,10 @@ namespace Uaflix.Controllers string BuildStreamUrl(OnlinesSettings init, string streamLink) { - string link = accsArgs(streamLink); + string link = StripLampacArgs(streamLink?.Trim()); + if (string.IsNullOrEmpty(link)) + return link; + if (ApnHelper.IsEnabled(init)) { if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link)) @@ -350,5 +353,21 @@ namespace Uaflix.Controllers return HostStreamProxy(init, link); } + + 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; + } } } diff --git a/Unimay/Controllers/Controller.cs b/Unimay/Controllers/Controller.cs index 49d34e0..0c692f9 100644 --- a/Unimay/Controllers/Controller.cs +++ b/Unimay/Controllers/Controller.cs @@ -104,7 +104,8 @@ namespace Unimay.Controllers if (string.IsNullOrEmpty(masterUrl)) return OnError("no stream"); - return UpdateService.Validate(Redirect(HostStreamProxy(init, accsArgs(masterUrl), proxy: proxyManager.Get()))); + string cleaned = StripLampacArgs(masterUrl?.Trim()); + return UpdateService.Validate(Redirect(HostStreamProxy(init, cleaned, proxy: proxyManager.Get()))); } if (itemType == "Фільм") @@ -140,5 +141,21 @@ namespace Unimay.Controllers return OnError("unsupported type"); }); } + + 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; + } } } From 43808b819f9d08a11c9a1bd87fe8315c11a11590 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 14:06:39 +0200 Subject: [PATCH 16/37] feat(makhno): support string-based APN configuration Allow APN configuration to be specified as a single string value in addition to the existing boolean + host format. When 'apn' is a string, it is used as the host and enabled is set automatically. --- Makhno/ApnHelper.cs | 20 ++++++++++++++++---- Makhno/ModInit.cs | 7 +++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Makhno/ApnHelper.cs b/Makhno/ApnHelper.cs index 394a5bc..13e9176 100644 --- a/Makhno/ApnHelper.cs +++ b/Makhno/ApnHelper.cs @@ -17,12 +17,24 @@ namespace Shared.Engine if (conf == null) return false; - if (!conf.TryGetValue("apn", out var apnToken) || apnToken?.Type != JTokenType.Boolean) + if (!conf.TryGetValue("apn", out var apnToken) || apnToken == null) return false; - enabled = apnToken.Value(); - host = conf.Value("apn_host"); - return true; + if (apnToken.Type == JTokenType.Boolean) + { + enabled = apnToken.Value(); + host = conf.Value("apn_host"); + return true; + } + + if (apnToken.Type == JTokenType.String) + { + host = apnToken.Value(); + enabled = !string.IsNullOrWhiteSpace(host); + return true; + } + + return false; } public static void ApplyInitConf(bool enabled, string host, BaseSettings init) diff --git a/Makhno/ModInit.cs b/Makhno/ModInit.cs index 23151a7..f6f95a5 100644 --- a/Makhno/ModInit.cs +++ b/Makhno/ModInit.cs @@ -54,8 +54,11 @@ namespace Makhno }; var conf = ModuleInvoke.Conf("Makhno", Makhno); bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost); - conf.Remove("apn"); - conf.Remove("apn_host"); + if (hasApn) + { + conf.Remove("apn"); + conf.Remove("apn_host"); + } Makhno = conf.ToObject(); if (hasApn) ApnHelper.ApplyInitConf(apnEnabled, apnHost, Makhno); From fed04725148ae9b28ecf12724399102d1093d85b Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 14:20:58 +0200 Subject: [PATCH 17/37] feat(makhno): support multiple language variants for categories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for Ukrainian, Russian, and English category names in both Makhno and UaTUT controllers. This improves internationalization by recognizing category names in different languages. Supported categories now include: - Series: Серіал, Сериал, Аніме, Аниме, Мультсеріал, Мультсериал, TV - Movies: Фільм, Фильм, Мультфільм, Мультфильм, Movie Refactored filtering logic to use dedicated helper methods for better code maintainability. --- Makhno/Controller.cs | 7 ++++++- UaTUT/Controller.cs | 25 +++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 3302487..a7ff918 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -344,7 +344,12 @@ namespace Makhno return false; 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("Мультсериал", StringComparison.OrdinalIgnoreCase) + || category.Equals("TV", StringComparison.OrdinalIgnoreCase); } private bool IsSerialByUrl(string url, int serial) diff --git a/UaTUT/Controller.cs b/UaTUT/Controller.cs index 29dadc3..19f6a11 100644 --- a/UaTUT/Controller.cs +++ b/UaTUT/Controller.cs @@ -66,7 +66,7 @@ namespace UaTUT var init = ModInit.UaTUT; // Фільтруємо тільки серіали та аніме - var seriesResults = searchResults.Where(r => r.Category == "Серіал" || r.Category == "Аніме").ToList(); + var seriesResults = searchResults.Where(r => IsSeriesCategory(r.Category)).ToList(); if (!seriesResults.Any()) { @@ -249,7 +249,7 @@ namespace UaTUT var init = ModInit.UaTUT; // Фільтруємо тільки фільми - var movieResults = searchResults.Where(r => r.Category == "Фільм").ToList(); + var movieResults = searchResults.Where(r => IsMovieCategory(r.Category)).ToList(); if (!movieResults.Any()) { @@ -456,6 +456,27 @@ namespace UaTUT return cleaned; } + private static bool IsMovieCategory(string category) + { + if (string.IsNullOrWhiteSpace(category)) + return false; + + var value = category.Trim().ToLowerInvariant(); + return value == "фільм" || value == "фильм" || value == "мультфільм" || value == "мультфильм" || value == "movie"; + } + + private static bool IsSeriesCategory(string category) + { + if (string.IsNullOrWhiteSpace(category)) + return false; + + var value = category.Trim().ToLowerInvariant(); + return value == "серіал" || value == "сериал" + || value == "аніме" || value == "аниме" + || value == "мультсеріал" || value == "мультсериал" + || value == "tv"; + } + string BuildStreamUrl(OnlinesSettings init, string streamLink) { string link = streamLink?.Trim(); From ac8bbe03183f6560b05bd2822c96738e5bff894d Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 14:33:41 +0200 Subject: [PATCH 18/37] feat(controllers): add conditional handling for anime categories Anime content classification now respects the serial/preferSeries parameter to determine whether to treat anime as a series or movie, improving content type detection accuracy across both Makhno and UaTUT controllers. --- Makhno/Controller.cs | 10 ++++++++-- Makhno/ModInit.cs | 2 +- UaTUT/Controller.cs | 30 ++++++++++++++++++++++-------- UaTUT/ModInit.cs | 2 +- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index a7ff918..eb23aff 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -326,7 +326,7 @@ namespace Makhno playUrl = invoke.BuildAshdiUrl(ashdiPath); - bool isSerial = serial == 1 || IsSerialByCategory(selected.Category) || IsSerialByUrl(playUrl, serial); + bool isSerial = serial == 1 || IsSerialByCategory(selected.Category, serial) || IsSerialByUrl(playUrl, serial); return new ResolveResult { @@ -338,11 +338,17 @@ namespace Makhno }; } - private bool IsSerialByCategory(string category) + 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) diff --git a/Makhno/ModInit.cs b/Makhno/ModInit.cs index f6f95a5..7417059 100644 --- a/Makhno/ModInit.cs +++ b/Makhno/ModInit.cs @@ -23,7 +23,7 @@ namespace Makhno { public class ModInit { - public static double Version => 1.0; + public static double Version => 1.5; public static OnlinesSettings Makhno; public static bool ApnHostProvided; diff --git a/UaTUT/Controller.cs b/UaTUT/Controller.cs index 19f6a11..13dac3a 100644 --- a/UaTUT/Controller.cs +++ b/UaTUT/Controller.cs @@ -53,20 +53,20 @@ namespace UaTUT if (serial == 1) { - return await HandleSeries(searchResults, imdb_id, kinopoisk_id, title, original_title, year, s, season, t, rjson, invoke); + return await HandleSeries(searchResults, imdb_id, kinopoisk_id, title, original_title, year, s, season, t, rjson, invoke, preferSeries: true); } else { - return await HandleMovie(searchResults, rjson, invoke); + return await HandleMovie(searchResults, rjson, invoke, preferSeries: false); } } - private async Task HandleSeries(List searchResults, string imdb_id, long kinopoisk_id, string title, string original_title, int year, int s, int season, string t, bool rjson, UaTUTInvoke invoke) + private async Task HandleSeries(List searchResults, string imdb_id, long kinopoisk_id, string title, string original_title, int year, int s, int season, string t, bool rjson, UaTUTInvoke invoke, bool preferSeries) { var init = ModInit.UaTUT; // Фільтруємо тільки серіали та аніме - var seriesResults = searchResults.Where(r => IsSeriesCategory(r.Category)).ToList(); + var seriesResults = searchResults.Where(r => IsSeriesCategory(r.Category, preferSeries)).ToList(); if (!seriesResults.Any()) { @@ -244,12 +244,12 @@ namespace UaTUT return match.Success ? int.Parse(match.Groups[1].Value) : 0; } - private async Task HandleMovie(List searchResults, bool rjson, UaTUTInvoke invoke) + private async Task HandleMovie(List searchResults, bool rjson, UaTUTInvoke invoke, bool preferSeries) { var init = ModInit.UaTUT; // Фільтруємо тільки фільми - var movieResults = searchResults.Where(r => IsMovieCategory(r.Category)).ToList(); + var movieResults = searchResults.Where(r => IsMovieCategory(r.Category, preferSeries)).ToList(); if (!movieResults.Any()) { @@ -456,27 +456,41 @@ namespace UaTUT return cleaned; } - private static bool IsMovieCategory(string category) + private static bool IsMovieCategory(string category, bool preferSeries) { if (string.IsNullOrWhiteSpace(category)) return false; var value = category.Trim().ToLowerInvariant(); + if (IsAnimeCategory(value)) + return !preferSeries; + return value == "фільм" || value == "фильм" || value == "мультфільм" || value == "мультфильм" || value == "movie"; } - private static bool IsSeriesCategory(string category) + private static bool IsSeriesCategory(string category, bool preferSeries) { if (string.IsNullOrWhiteSpace(category)) return false; var value = category.Trim().ToLowerInvariant(); + if (IsAnimeCategory(value)) + return preferSeries; + return value == "серіал" || value == "сериал" || value == "аніме" || value == "аниме" || value == "мультсеріал" || value == "мультсериал" || value == "tv"; } + private static bool IsAnimeCategory(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + return value == "аніме" || value == "аниме"; + } + string BuildStreamUrl(OnlinesSettings init, string streamLink) { string link = streamLink?.Trim(); diff --git a/UaTUT/ModInit.cs b/UaTUT/ModInit.cs index f2a12ed..8813e0b 100644 --- a/UaTUT/ModInit.cs +++ b/UaTUT/ModInit.cs @@ -24,7 +24,7 @@ namespace UaTUT { public class ModInit { - public static double Version => 3.3; + public static double Version => 3.4; public static OnlinesSettings UaTUT; public static bool ApnHostProvided; From c6cc802403febf0025cbcaf3f5772711fbb15d1d Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 19:22:24 +0200 Subject: [PATCH 19/37] fix(makhno): handle voices with different season counts Previously, the code assumed all voices had the same number of seasons as the first voice, which could cause errors or incorrect behavior when voices had varying season counts. Now calculates the maximum seasons across all voices and safely handles cases where a specific voice doesn't have the requested season index. --- Makhno/Controller.cs | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index eb23aff..00e1fdb 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -196,13 +196,16 @@ namespace Makhno return OnError(); } + int maxSeasons = playerData.Voices.Max(v => v.Seasons?.Count ?? 0); + if (maxSeasons <= 0) + return OnError(); + if (season == -1) { - var firstVoice = playerData.Voices.First(); var season_tpl = new SeasonTpl(); - for (int i = 0; i < firstVoice.Seasons.Count; i++) + for (int i = 0; i < maxSeasons; i++) { - var seasonItem = firstVoice.Seasons[i]; + var seasonItem = playerData.Voices.Select(v => v.Seasons.ElementAtOrDefault(i)).FirstOrDefault(s => s != null); string seasonName = seasonItem.Title ?? $"Сезон {i + 1}"; int seasonNumber = 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={seasonNumber}"; @@ -213,7 +216,7 @@ namespace Makhno } int seasonIndex = season > 0 ? season - 1 : season; - if (seasonIndex < 0 || seasonIndex >= playerData.Voices.First().Seasons.Count) + if (seasonIndex < 0 || seasonIndex >= maxSeasons) return OnError(); var voice_tpl = new VoiceTpl(); @@ -229,7 +232,11 @@ namespace Makhno { var voice = playerData.Voices[i]; string voiceName = voice.Name ?? $"Озвучка {i + 1}"; - int seasonNumber = seasonIndex + 1; + int voiceSeasonIndex = GetSeasonIndexForVoice(voice, seasonIndex); + if (voiceSeasonIndex < 0) + continue; + + int seasonNumber = voiceSeasonIndex + 1; string voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}&t={i}"; bool isActive = selectedVoice == i.ToString(); voice_tpl.Append(voiceName, isActive, voiceLink); @@ -238,9 +245,10 @@ namespace Makhno if (!string.IsNullOrEmpty(selectedVoice) && int.TryParse(selectedVoice, out int voiceIndex) && voiceIndex < playerData.Voices.Count) { var selectedVoiceData = playerData.Voices[voiceIndex]; - if (seasonIndex < selectedVoiceData.Seasons.Count) + int effectiveSeasonIndex = GetSeasonIndexForVoice(selectedVoiceData, seasonIndex); + if (effectiveSeasonIndex >= 0) { - var selectedSeason = selectedVoiceData.Seasons[seasonIndex]; + var selectedSeason = selectedVoiceData.Seasons[effectiveSeasonIndex]; var sortedEpisodes = selectedSeason.Episodes.OrderBy(e => ExtractEpisodeNumber(e.Title)).ToList(); for (int i = 0; i < sortedEpisodes.Count; i++) @@ -249,7 +257,7 @@ namespace Makhno if (!string.IsNullOrEmpty(episode.File)) { string streamUrl = BuildStreamUrl(init, episode.File); - int seasonNumber = seasonIndex + 1; + int seasonNumber = effectiveSeasonIndex + 1; episode_tpl.Append( episode.Title, title ?? original_title, @@ -278,6 +286,17 @@ namespace Makhno return match.Success ? int.Parse(match.Groups[1].Value) : 0; } + private int GetSeasonIndexForVoice(Voice voice, int requestedSeasonIndex) + { + if (voice?.Seasons == null || voice.Seasons.Count == 0) + return -1; + + if (requestedSeasonIndex >= 0 && requestedSeasonIndex < voice.Seasons.Count) + return requestedSeasonIndex; + + return 0; + } + private async Task ResolvePlaySource(string imdbId, string title, string originalTitle, int year, int serial, MakhnoInvoke invoke) { string playUrl = null; From e3aa03089cc679114b956eea5741c72f419de4ec Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 19:28:32 +0200 Subject: [PATCH 20/37] fix(makhno): handle voices with different season counts and missing season data The previous implementation assumed all voices had the same number of seasons and used index-based access, which caused issues when voices had different season counts. The new implementation: - Extracts season numbers from season titles using regex - Creates a unified list of all available season numbers across all voices - Handles cases where voices have no seasons or missing season data - Selects appropriate season numbers when specific seasons are requested - Maintains backward compatibility with existing URL parameters This fixes issues with season selection when different voice tracks have varying season counts or when some voices lack season information entirely. --- Makhno/Controller.cs | 84 +++++++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 00e1fdb..57c7d38 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -196,18 +196,35 @@ namespace Makhno return OnError(); } - int maxSeasons = playerData.Voices.Max(v => v.Seasons?.Count ?? 0); - if (maxSeasons <= 0) + var voiceSeasons = playerData.Voices + .Select((voice, index) => new + { + Voice = voice, + Index = index, + Seasons = GetSeasonsWithNumbers(voice) + }) + .Where(v => v.Seasons.Count > 0) + .ToList(); + + 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) { var season_tpl = new SeasonTpl(); - for (int i = 0; i < maxSeasons; i++) + foreach (var seasonNumber in seasonNumbers) { - var seasonItem = playerData.Voices.Select(v => v.Seasons.ElementAtOrDefault(i)).FirstOrDefault(s => s != null); - string seasonName = seasonItem.Title ?? $"Сезон {i + 1}"; - int seasonNumber = i + 1; + var seasonItem = voiceSeasons + .SelectMany(v => v.Seasons) + .FirstOrDefault(s => s.Number == seasonNumber); + + 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}"; season_tpl.Append(seasonName, link, seasonNumber.ToString()); } @@ -215,28 +232,28 @@ namespace Makhno return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8"); } - int seasonIndex = season > 0 ? season - 1 : season; - if (seasonIndex < 0 || seasonIndex >= maxSeasons) - return OnError(); - var voice_tpl = new VoiceTpl(); var episode_tpl = new EpisodeTpl(); string selectedVoice = t; - if (string.IsNullOrEmpty(selectedVoice) && playerData.Voices.Any()) + if (string.IsNullOrEmpty(selectedVoice) || !int.TryParse(selectedVoice, out _)) { - selectedVoice = "0"; + var voiceWithSeason = voiceSeasons.FirstOrDefault(v => v.Seasons.Any(s => s.Number == season)); + 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}"; - int voiceSeasonIndex = GetSeasonIndexForVoice(voice, seasonIndex); - if (voiceSeasonIndex < 0) + var seasonsForVoice = GetSeasonsWithNumbers(voice); + if (seasonsForVoice.Count == 0) continue; - int seasonNumber = voiceSeasonIndex + 1; + int seasonNumber = seasonsForVoice.Any(s => s.Number == season) + ? season + : seasonsForVoice.Min(s => s.Number); + string voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}&t={i}"; bool isActive = selectedVoice == i.ToString(); voice_tpl.Append(voiceName, isActive, voiceLink); @@ -245,10 +262,14 @@ namespace Makhno if (!string.IsNullOrEmpty(selectedVoice) && int.TryParse(selectedVoice, out int voiceIndex) && voiceIndex < playerData.Voices.Count) { var selectedVoiceData = playerData.Voices[voiceIndex]; - int effectiveSeasonIndex = GetSeasonIndexForVoice(selectedVoiceData, seasonIndex); - if (effectiveSeasonIndex >= 0) + var seasonsForVoice = GetSeasonsWithNumbers(selectedVoiceData); + if (seasonsForVoice.Count > 0) { - var selectedSeason = selectedVoiceData.Seasons[effectiveSeasonIndex]; + int effectiveSeasonNumber = seasonsForVoice.Any(s => s.Number == season) + ? season + : seasonsForVoice.Min(s => s.Number); + + 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++) @@ -257,11 +278,10 @@ namespace Makhno if (!string.IsNullOrEmpty(episode.File)) { string streamUrl = BuildStreamUrl(init, episode.File); - int seasonNumber = effectiveSeasonIndex + 1; episode_tpl.Append( episode.Title, title ?? original_title, - seasonNumber.ToString(), + effectiveSeasonNumber.ToString(), (i + 1).ToString("D2"), streamUrl ); @@ -286,15 +306,29 @@ namespace Makhno return match.Success ? int.Parse(match.Groups[1].Value) : 0; } - private int GetSeasonIndexForVoice(Voice voice, int requestedSeasonIndex) + 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 -1; + return result; - if (requestedSeasonIndex >= 0 && requestedSeasonIndex < voice.Seasons.Count) - return requestedSeasonIndex; + 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 0; + return result; } private async Task ResolvePlaySource(string imdbId, string title, string originalTitle, int year, int serial, MakhnoInvoke invoke) From 097b42023cfd20a8781cc962b21015bd431ea2e1 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 19:34:57 +0200 Subject: [PATCH 21/37] fix(makhno): redirect to valid season when requested season is unavailable When a user requests a season that doesn't exist for a selected voice, the system now redirects to the first available season for that voice instead of silently using the first season. This ensures users are always directed to valid content and prevents confusion when season data is inconsistent across different voice options. --- Makhno/Controller.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 57c7d38..236247f 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -235,10 +235,12 @@ namespace Makhno 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 == season)); + var voiceWithSeason = voiceSeasons.FirstOrDefault(v => v.Seasons.Any(s => s.Number == requestedSeason)); selectedVoice = voiceWithSeason != null ? voiceWithSeason.Index.ToString() : voiceSeasons.First().Index.ToString(); } @@ -250,8 +252,8 @@ namespace Makhno if (seasonsForVoice.Count == 0) continue; - int seasonNumber = seasonsForVoice.Any(s => s.Number == season) - ? season + int seasonNumber = seasonsForVoice.Any(s => s.Number == requestedSeason) + ? requestedSeason : seasonsForVoice.Min(s => s.Number); string voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}&t={i}"; @@ -265,10 +267,16 @@ namespace Makhno var seasonsForVoice = GetSeasonsWithNumbers(selectedVoiceData); if (seasonsForVoice.Count > 0) { - int effectiveSeasonNumber = seasonsForVoice.Any(s => s.Number == season) - ? season + 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(); From 2516f8b8f64614d5f1c954919ac56ecb36f584b8 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 19:42:25 +0200 Subject: [PATCH 22/37] fix(makhno): add voice parameter to season links when preferred voice is available --- Makhno/Controller.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 236247f..b2b105f 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -224,8 +224,10 @@ namespace Makhno .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}"; + 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()); } From e2bc12b009823e8cc81c688e459c2b15f5b77b90 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 19:48:49 +0200 Subject: [PATCH 23/37] fix(makhno): handle season number filtering based on voice selection --- Makhno/Controller.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index b2b105f..65512a2 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -217,6 +217,18 @@ namespace Makhno 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) { From e9a4d19d8cc9d889a8b9fd80f5941d525d77e7d9 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 19:54:27 +0200 Subject: [PATCH 24/37] fix(makhno): handle season filtering when requested season is unavailable When a requested season is not available in the selected voice, redirect to season list instead of showing foreign seasons. This prevents displaying seasons from other voices that the user may not have access to. --- Makhno/Controller.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 65512a2..d60b0ba 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -266,11 +266,17 @@ namespace Makhno if (seasonsForVoice.Count == 0) continue; - int seasonNumber = seasonsForVoice.Any(s => s.Number == requestedSeason) - ? requestedSeason - : seasonsForVoice.Min(s => s.Number); - - string voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}&t={i}"; + bool hasRequestedSeason = seasonsForVoice.Any(s => s.Number == requestedSeason); + string voiceLink; + if (hasRequestedSeason) + { + voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={requestedSeason}&t={i}"; + } + else + { + // Force season list for this voice to avoid showing чужі сезони + 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}"; + } bool isActive = selectedVoice == i.ToString(); voice_tpl.Append(voiceName, isActive, voiceLink); } From 5348f3a9a98996d0fa9cb99e9e544384745f05c4 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 20:02:36 +0200 Subject: [PATCH 25/37] fix(makhno): add debug logging for season filtering logic Added debug logging to track voice selection and season filtering behavior during season number filtering. This helps diagnose issues with voice-season relationships and filtering logic. --- Makhno/Controller.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index d60b0ba..0f68016 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -206,6 +206,13 @@ namespace Makhno .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() From f1893574478cefb3439da2c60ab2d12d9d740be3 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 20:45:19 +0200 Subject: [PATCH 26/37] fix(makhno): correct season link generation for multi-season voices The logic for generating voice links was incorrect - it was checking for requested season availability instead of checking if there are multiple seasons available. This caused season lists to not show properly for multi-season voices. The fix ensures that: - Multi-season voices always show the season list to keep filters correct - Single-season voices link directly to that season - The season parameter is correctly set based on the actual number of seasons --- Makhno/Controller.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 0f68016..b786ded 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -273,16 +273,16 @@ namespace Makhno if (seasonsForVoice.Count == 0) continue; - bool hasRequestedSeason = seasonsForVoice.Any(s => s.Number == requestedSeason); string voiceLink; - if (hasRequestedSeason) + if (seasonsForVoice.Count > 1) { - voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={requestedSeason}&t={i}"; + // 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 { - // Force season list for this voice to avoid showing чужі сезони - 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}"; + 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); From 55ee3b644db187b6c22f4ca8bb450c3122f7bbfa Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 20:55:58 +0200 Subject: [PATCH 27/37] fix(makhno): remove debug logging and improve season filtering logic - Removed debug logging statements that were causing noisy output in production - Simplified season filtering logic by using nullable int for seasonVoiceIndex - Improved season link generation to handle requested season availability more efficiently - Refactored season item selection logic to be more maintainable and readable --- Makhno/Controller.cs | 56 ++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index b786ded..303754f 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -206,12 +206,7 @@ namespace Makhno .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}]"); - } + // Debug logging disabled to avoid noisy output in production. var seasonNumbers = voiceSeasons .SelectMany(v => v.Seasons.Select(s => s.Number)) @@ -224,9 +219,13 @@ namespace Makhno if (season == -1) { - if (int.TryParse(t, out int seasonVoiceIndex) && seasonVoiceIndex >= 0 && seasonVoiceIndex < playerData.Voices.Count) + int? seasonVoiceIndex = null; + if (int.TryParse(t, out int tIndex) && tIndex >= 0 && tIndex < playerData.Voices.Count) + seasonVoiceIndex = tIndex; + + if (seasonVoiceIndex.HasValue) { - var seasonsForVoice = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndex]) + var seasonsForVoice = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndex.Value]) .Select(s => s.Number) .Distinct() .OrderBy(n => n) @@ -239,12 +238,20 @@ namespace Makhno var season_tpl = new SeasonTpl(); foreach (var seasonNumber in seasonNumbers) { - var seasonItem = voiceSeasons - .SelectMany(v => v.Seasons) - .FirstOrDefault(s => s.Number == seasonNumber); + (Season Season, int Number)? seasonItem = null; + if (seasonVoiceIndex.HasValue) + { + var voiceSeasonsForT = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndex.Value]); + seasonItem = voiceSeasonsForT.FirstOrDefault(s => s.Number == seasonNumber); + } + else + { + 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 voiceParam = seasonVoiceIndex.HasValue ? $"&t={seasonVoiceIndex.Value}" : 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()); @@ -274,16 +281,16 @@ namespace Makhno continue; string voiceLink; - if (seasonsForVoice.Count > 1) + bool hasRequestedSeason = seasonsForVoice.Any(s => s.Number == requestedSeason); + if (hasRequestedSeason) { - // 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}"; + voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={requestedSeason}&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}"; + 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}"; } + bool isActive = selectedVoice == i.ToString(); voice_tpl.Append(voiceName, isActive, voiceLink); } @@ -294,17 +301,14 @@ namespace Makhno 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) + bool hasRequestedSeason = seasonsForVoice.Any(s => s.Number == requestedSeason); + if (!hasRequestedSeason) { - 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}"; + string redirectUrl = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season=-1&t={voiceIndex}"; return UpdateService.Validate(Redirect(redirectUrl)); } - var selectedSeason = seasonsForVoice.First(s => s.Number == effectiveSeasonNumber).Season; + var selectedSeason = seasonsForVoice.First(s => s.Number == requestedSeason).Season; var sortedEpisodes = selectedSeason.Episodes.OrderBy(e => ExtractEpisodeNumber(e.Title)).ToList(); for (int i = 0; i < sortedEpisodes.Count; i++) @@ -316,7 +320,7 @@ namespace Makhno episode_tpl.Append( episode.Title, title ?? original_title, - effectiveSeasonNumber.ToString(), + requestedSeason.ToString(), (i + 1).ToString("D2"), streamUrl ); From ac9242308f85441077158d9b46a9f9c671e501ca Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 20:58:48 +0200 Subject: [PATCH 28/37] fix(makhno): handle null season items when filtering by season number The code now properly handles cases where the requested season is not found in the voice seasons list by checking if the match has a valid Season before accessing its properties. This prevents potential null reference exceptions when generating season links. --- Makhno/Controller.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 303754f..ce126f0 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -242,17 +242,19 @@ namespace Makhno if (seasonVoiceIndex.HasValue) { var voiceSeasonsForT = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndex.Value]); - seasonItem = voiceSeasonsForT.FirstOrDefault(s => s.Number == seasonNumber); + var match = voiceSeasonsForT.FirstOrDefault(s => s.Number == seasonNumber); + seasonItem = match.Season != null ? match : ((Season Season, int Number)?)null; } else { - seasonItem = voiceSeasons + var match = voiceSeasons .SelectMany(v => v.Seasons) .FirstOrDefault(s => s.Number == seasonNumber); + seasonItem = match.Season != null ? match : ((Season Season, int Number)?)null; } string voiceParam = seasonVoiceIndex.HasValue ? $"&t={seasonVoiceIndex.Value}" : string.Empty; - string seasonName = seasonItem.Season?.Title ?? $"Сезон {seasonNumber}"; + string seasonName = seasonItem.HasValue ? seasonItem.Value.Season?.Title ?? $"Сезон {seasonNumber}" : $"Сезон {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()); } From bc4016b7c3f605053922605e9f64eba6dd585c9b Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 21:21:07 +0200 Subject: [PATCH 29/37] feat(makhno): enhance season template generation with voice-specific filtering Implements dynamic season template building that respects voice-specific season availability. Adds proper handling of voice indices and creates filtered season lists based on available content for each voice option. This ensures season navigation remains synchronized when switching between different voice tracks with varying season availability. --- Makhno/Controller.cs | 46 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index ce126f0..b2b25e2 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -267,12 +267,55 @@ namespace Makhno int requestedSeason = seasonNumbers.Contains(season) ? season : seasonNumbers.First(); + int? seasonVoiceIndexForTpl = null; string selectedVoice = t; - if (string.IsNullOrEmpty(selectedVoice) || !int.TryParse(selectedVoice, out _)) + if (string.IsNullOrEmpty(selectedVoice) || !int.TryParse(selectedVoice, out int selectedVoiceIndex)) { var voiceWithSeason = voiceSeasons.FirstOrDefault(v => v.Seasons.Any(s => s.Number == requestedSeason)); selectedVoice = voiceWithSeason != null ? voiceWithSeason.Index.ToString() : voiceSeasons.First().Index.ToString(); } + else if (selectedVoiceIndex >= 0 && selectedVoiceIndex < playerData.Voices.Count) + { + seasonVoiceIndexForTpl = selectedVoiceIndex; + } + + // Build season template for selected voice (if valid) to keep season list in sync when switching voices. + var season_tpl = new SeasonTpl(); + List seasonNumbersForTpl = seasonNumbers; + if (seasonVoiceIndexForTpl.HasValue) + { + var seasonsForVoiceTpl = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndexForTpl.Value]) + .Select(s => s.Number) + .Distinct() + .OrderBy(n => n) + .ToList(); + + if (seasonsForVoiceTpl.Count > 0) + seasonNumbersForTpl = seasonsForVoiceTpl; + } + + foreach (var seasonNumber in seasonNumbersForTpl) + { + (Season Season, int Number)? seasonItem = null; + if (seasonVoiceIndexForTpl.HasValue) + { + var voiceSeasonsForT = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndexForTpl.Value]); + var match = voiceSeasonsForT.FirstOrDefault(s => s.Number == seasonNumber); + seasonItem = match.Season != null ? match : ((Season Season, int Number)?)null; + } + else + { + var match = voiceSeasons + .SelectMany(v => v.Seasons) + .FirstOrDefault(s => s.Number == seasonNumber); + seasonItem = match.Season != null ? match : ((Season Season, int Number)?)null; + } + + string voiceParam = seasonVoiceIndexForTpl.HasValue ? $"&t={seasonVoiceIndexForTpl.Value}" : string.Empty; + string seasonName = seasonItem.HasValue ? seasonItem.Value.Season?.Title ?? $"Сезон {seasonNumber}" : $"Сезон {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()); + } for (int i = 0; i < playerData.Voices.Count; i++) { @@ -331,6 +374,7 @@ namespace Makhno } } + episode_tpl.Append(season_tpl); episode_tpl.Append(voice_tpl); if (rjson) return Content(episode_tpl.ToJson(), "application/json; charset=utf-8"); From f62ed52434bfd9c71668f25b1fd7e6dc5d25a7c5 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 21:32:32 +0200 Subject: [PATCH 30/37] refactor(makhno): rename season template variable and update html content generation --- Makhno/Controller.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index b2b25e2..1bff06a 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -280,7 +280,7 @@ namespace Makhno } // Build season template for selected voice (if valid) to keep season list in sync when switching voices. - var season_tpl = new SeasonTpl(); + var seasonTplForVoice = new SeasonTpl(); List seasonNumbersForTpl = seasonNumbers; if (seasonVoiceIndexForTpl.HasValue) { @@ -314,7 +314,7 @@ namespace Makhno string voiceParam = seasonVoiceIndexForTpl.HasValue ? $"&t={seasonVoiceIndexForTpl.Value}" : string.Empty; string seasonName = seasonItem.HasValue ? seasonItem.Value.Season?.Title ?? $"Сезон {seasonNumber}" : $"Сезон {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()); + seasonTplForVoice.Append(seasonName, link, seasonNumber.ToString()); } for (int i = 0; i < playerData.Voices.Count; i++) @@ -374,12 +374,11 @@ namespace Makhno } } - episode_tpl.Append(season_tpl); 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"); + return Content(seasonTplForVoice.ToHtml() + episode_tpl.ToHtml(), "text/html; charset=utf-8"); } private int ExtractEpisodeNumber(string title) From 90574ad919cd9194275fc35fcef8f846ac3ff285 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 21:38:57 +0200 Subject: [PATCH 31/37] feat(makhno): add season set validation for voice selection consistency Add logic to validate that the current voice's available seasons match the selected voice season set before generating voice links. This ensures consistent behavior when switching between voices with different season availability. --- Makhno/Controller.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 1bff06a..810e86e 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -279,6 +279,18 @@ namespace Makhno seasonVoiceIndexForTpl = selectedVoiceIndex; } + HashSet selectedVoiceSeasonSet = null; + if (seasonVoiceIndexForTpl.HasValue) + { + selectedVoiceSeasonSet = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndexForTpl.Value]) + .Select(s => s.Number) + .ToHashSet(); + } + else + { + selectedVoiceSeasonSet = seasonNumbers.ToHashSet(); + } + // Build season template for selected voice (if valid) to keep season list in sync when switching voices. var seasonTplForVoice = new SeasonTpl(); List seasonNumbersForTpl = seasonNumbers; @@ -327,7 +339,8 @@ namespace Makhno string voiceLink; bool hasRequestedSeason = seasonsForVoice.Any(s => s.Number == requestedSeason); - if (hasRequestedSeason) + bool sameSeasonSet = seasonsForVoice.Select(s => s.Number).ToHashSet().SetEquals(selectedVoiceSeasonSet); + if (hasRequestedSeason && sameSeasonSet) { voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={requestedSeason}&t={i}"; } From 6ebd793e909d616142b989882e30ac1de10fcbf6 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 21:43:35 +0200 Subject: [PATCH 32/37] chore(makhno): bump version from 1.5 to 1.6 --- Makhno/ModInit.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makhno/ModInit.cs b/Makhno/ModInit.cs index 7417059..bc5b7a0 100644 --- a/Makhno/ModInit.cs +++ b/Makhno/ModInit.cs @@ -23,7 +23,7 @@ namespace Makhno { public class ModInit { - public static double Version => 1.5; + public static double Version => 1.6; public static OnlinesSettings Makhno; public static bool ApnHostProvided; From f2b70fa95e48cd3bd6b0f14663c169a0d4acddce Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Wed, 4 Feb 2026 21:52:22 +0200 Subject: [PATCH 33/37] feat(mikai,uaflix): implement voice-specific season filtering and redirect handling Add support for restricting season lists by specific voice selection, implement proper redirect handling when selected season is unavailable for chosen voice, and add season set validation to ensure consistent navigation between voices with different season availability --- Mikai/Controller.cs | 43 ++++++++++++++++++++++----- Mikai/ModInit.cs | 2 +- Uaflix/Controller.cs | 71 +++++++++++++++++++++++++++++++++++++++----- Uaflix/ModInit.cs | 2 +- 4 files changed, 102 insertions(+), 16 deletions(-) diff --git a/Mikai/Controller.cs b/Mikai/Controller.cs index c8cb05a..5ec5ddf 100644 --- a/Mikai/Controller.cs +++ b/Mikai/Controller.cs @@ -57,11 +57,14 @@ namespace Mikai.Controllers if (isSerial) { - var seasonNumbers = voices.Values - .SelectMany(v => v.Seasons.Keys) - .Distinct() - .OrderBy(n => n) - .ToList(); + bool restrictByVoice = !string.IsNullOrEmpty(t) && voices.TryGetValue(t, out var 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", _proxyManager); @@ -72,6 +75,8 @@ namespace Mikai.Controllers foreach (var seasonNumber in seasonNumbers) { string link = $"{host}/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()); } @@ -89,16 +94,29 @@ namespace Mikai.Controllers 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) { - string voiceLink = $"{host}/mikai?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)}"; + var targetSeasonSet = GetSeasonSet(voice.Value); + bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet); + string voiceLink = $"{host}/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)) - return OnError("mikai", _proxyManager); + { + string redirectUrl = $"{host}/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)) @@ -367,6 +385,17 @@ namespace Mikai.Controllers 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)) diff --git a/Mikai/ModInit.cs b/Mikai/ModInit.cs index eb2a898..c9ef993 100644 --- a/Mikai/ModInit.cs +++ b/Mikai/ModInit.cs @@ -24,7 +24,7 @@ namespace Mikai { public class ModInit { - public static double Version => 3.3; + public static double Version => 3.4; public static OnlinesSettings Mikai; public static bool ApnHostProvided; diff --git a/Uaflix/Controller.cs b/Uaflix/Controller.cs index be1ed92..bf70816 100644 --- a/Uaflix/Controller.cs +++ b/Uaflix/Controller.cs @@ -167,11 +167,21 @@ namespace Uaflix.Controllers // s == -1: Вибір сезону if (s == -1) { - var allSeasons = structure.Voices - .SelectMany(v => v.Value.Seasons.Keys) - .Distinct() - .OrderBy(sn => sn) - .ToList(); + List allSeasons; + bool restrictByVoice = !string.IsNullOrEmpty(t) && structure.Voices.TryGetValue(t, out var tVoice) && IsAshdiVoice(tVoice); + if (restrictByVoice) + { + allSeasons = GetSeasonSet(tVoice).OrderBy(sn => sn).ToList(); + OnLog($"Ashdi voice selected (t='{t}'), seasons count={allSeasons.Count}"); + } + else + { + allSeasons = structure.Voices + .SelectMany(v => GetSeasonSet(v.Value)) + .Distinct() + .OrderBy(sn => sn) + .ToList(); + } OnLog($"Found {allSeasons.Count} seasons in structure: {string.Join(", ", allSeasons)}"); @@ -205,6 +215,8 @@ namespace Uaflix.Controllers foreach (var season in seasonsWithValidEpisodes) { string link = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season}&href={HttpUtility.UrlEncode(filmUrl)}"; + if (restrictByVoice) + link += $"&t={HttpUtility.UrlEncode(t)}"; season_tpl.Append($"{season}", link, season.ToString()); OnLog($"Added season {season} to template"); } @@ -239,12 +251,31 @@ namespace Uaflix.Controllers t = voicesForSeason[0].DisplayName; OnLog($"Auto-selected first voice: {t}"); } - + else if (!structure.Voices.ContainsKey(t)) + { + t = voicesForSeason[0].DisplayName; + OnLog($"Voice '{t}' not found, fallback to first voice: {t}"); + } + // Створюємо VoiceTpl з усіма озвучками var voice_tpl = new VoiceTpl(); + var selectedVoiceInfo = structure.Voices[t]; + var selectedSeasonSet = GetSeasonSet(selectedVoiceInfo); + bool selectedIsAshdi = IsAshdiVoice(selectedVoiceInfo); + foreach (var voice in voicesForSeason) { - string voiceLink = $"{host}/uaflix?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.DisplayName)}&href={HttpUtility.UrlEncode(filmUrl)}"; + bool targetIsAshdi = IsAshdiVoice(voice.Info); + var targetSeasonSet = GetSeasonSet(voice.Info); + bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet); + bool needSeasonReset = (selectedIsAshdi || targetIsAshdi) && !sameSeasonSet; + + string voiceLink = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&href={HttpUtility.UrlEncode(filmUrl)}"; + if (needSeasonReset) + voiceLink += $"&s=-1&t={HttpUtility.UrlEncode(voice.DisplayName)}"; + else + voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.DisplayName)}"; + bool isActive = voice.DisplayName == t; voice_tpl.Append(voice.DisplayName, isActive, voiceLink); } @@ -261,6 +292,13 @@ namespace Uaflix.Controllers if (!structure.Voices[t].Seasons.ContainsKey(s)) { OnLog($"Season {s} not found for voice '{t}'"); + if (IsAshdiVoice(structure.Voices[t])) + { + string redirectUrl = $"{host}/uaflix?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)}&href={HttpUtility.UrlEncode(filmUrl)}"; + OnLog($"Ashdi voice missing season, redirect to season selector: {redirectUrl}"); + return Redirect(redirectUrl); + } + OnLog("=== RETURN: season not found for voice OnError ==="); return OnError("uaflix", proxyManager); } @@ -369,5 +407,24 @@ namespace Uaflix.Controllers cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&'); return cleaned; } + + private static bool IsAshdiVoice(VoiceInfo voice) + { + if (voice == null || string.IsNullOrEmpty(voice.PlayerType)) + return false; + + return voice.PlayerType == "ashdi-serial" || voice.PlayerType == "ashdi-vod"; + } + + private static HashSet GetSeasonSet(VoiceInfo 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.File))) + .Select(kv => kv.Key) + .ToHashSet(); + } } } diff --git a/Uaflix/ModInit.cs b/Uaflix/ModInit.cs index 50dbf56..370547b 100644 --- a/Uaflix/ModInit.cs +++ b/Uaflix/ModInit.cs @@ -25,7 +25,7 @@ namespace Uaflix { public class ModInit { - public static double Version => 3.3; + public static double Version => 3.4; public static OnlinesSettings UaFlix; public static bool ApnHostProvided; From 6ea1a5febe71f0795f8fc953eddc8fb06cddfae4 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Thu, 5 Feb 2026 08:41:35 +0200 Subject: [PATCH 34/37] refactor(mikai,uaflix): explicitly declare voice info variables before assignment Changed implicit variable declarations to explicit type declarations for voiceForSeasons and tVoice variables to improve code clarity and maintain consistent typing patterns in both controllers. --- Mikai/Controller.cs | 3 ++- Uaflix/Controller.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Mikai/Controller.cs b/Mikai/Controller.cs index 5ec5ddf..90fdaef 100644 --- a/Mikai/Controller.cs +++ b/Mikai/Controller.cs @@ -57,7 +57,8 @@ namespace Mikai.Controllers if (isSerial) { - bool restrictByVoice = !string.IsNullOrEmpty(t) && voices.TryGetValue(t, out var voiceForSeasons); + MikaiVoiceInfo voiceForSeasons = null; + bool restrictByVoice = !string.IsNullOrEmpty(t) && voices.TryGetValue(t, out voiceForSeasons); var seasonNumbers = restrictByVoice ? GetSeasonSet(voiceForSeasons).OrderBy(n => n).ToList() : voices.Values diff --git a/Uaflix/Controller.cs b/Uaflix/Controller.cs index bf70816..4ca2558 100644 --- a/Uaflix/Controller.cs +++ b/Uaflix/Controller.cs @@ -168,7 +168,8 @@ namespace Uaflix.Controllers if (s == -1) { List allSeasons; - bool restrictByVoice = !string.IsNullOrEmpty(t) && structure.Voices.TryGetValue(t, out var tVoice) && IsAshdiVoice(tVoice); + VoiceInfo tVoice = null; + bool restrictByVoice = !string.IsNullOrEmpty(t) && structure.Voices.TryGetValue(t, out tVoice) && IsAshdiVoice(tVoice); if (restrictByVoice) { allSeasons = GetSeasonSet(tVoice).OrderBy(sn => sn).ToList(); From 371a54f759b23cbacfeaf44d5ab607d8d966ddb3 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Fri, 6 Feb 2026 17:14:54 +0200 Subject: [PATCH 35/37] feat(controllers): add checksearch parameter support across all streaming service controllers Add checksearch functionality to validate online search availability for multiple streaming services including AnimeON, Bamboo, CikavaIdeya, Makhno, Mikai, StarLight, UAKino, UaTUT, Uaflix, and Unimay controllers. Each controller now supports a checksearch parameter that returns appropriate responses when online search validation is enabled. --- AnimeON/Controller.cs | 15 ++++++++++++++- Bamboo/Controller.cs | 14 +++++++++++++- CikavaIdeya/Controller.cs | 14 +++++++++++++- Makhno/Controller.cs | 7 ++++++- Mikai/Controller.cs | 15 ++++++++++++++- StarLight/Controller.cs | 14 +++++++++++++- UAKino/Controller.cs | 14 +++++++++++++- UaTUT/Controller.cs | 13 ++++++++++++- Uaflix/Controller.cs | 3 +++ Unimay/Controllers/Controller.cs | 14 +++++++++++++- 10 files changed, 114 insertions(+), 9 deletions(-) diff --git a/AnimeON/Controller.cs b/AnimeON/Controller.cs index ef8ca26..a4307db 100644 --- a/AnimeON/Controller.cs +++ b/AnimeON/Controller.cs @@ -28,7 +28,7 @@ namespace AnimeON.Controllers [HttpGet] [Route("animeon")] - async public 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) + async public 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); @@ -37,6 +37,19 @@ namespace AnimeON.Controllers return Forbid(); var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager); + + if (checksearch) + { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError("animeon", proxyManager); + + var seasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial); + if (seasons != null && seasons.Count > 0) + return Content("data-json=", "text/plain; charset=utf-8"); + + return OnError("animeon", proxyManager); + } + 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); diff --git a/Bamboo/Controller.cs b/Bamboo/Controller.cs index 2d069db..f2a993b 100644 --- a/Bamboo/Controller.cs +++ b/Bamboo/Controller.cs @@ -24,7 +24,7 @@ namespace Bamboo.Controllers [HttpGet] [Route("bamboo")] - async public 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, string href = null) + async public 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, string href = null, bool checksearch = false) { await UpdateService.ConnectAsync(host); @@ -34,6 +34,18 @@ namespace Bamboo.Controllers var invoke = new BambooInvoke(init, hybridCache, OnLog, proxyManager); + if (checksearch) + { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError("bamboo", proxyManager); + + var searchResults = await invoke.Search(title, original_title); + if (searchResults != null && searchResults.Count > 0) + return Content("data-json=", "text/plain; charset=utf-8"); + + return OnError("bamboo", proxyManager); + } + string itemUrl = href; if (string.IsNullOrEmpty(itemUrl)) { diff --git a/CikavaIdeya/Controller.cs b/CikavaIdeya/Controller.cs index 25fb9c3..e0612dd 100644 --- a/CikavaIdeya/Controller.cs +++ b/CikavaIdeya/Controller.cs @@ -27,7 +27,7 @@ namespace CikavaIdeya.Controllers [HttpGet] [Route("cikavaideya")] - async public 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 e = -1, bool play = false, bool rjson = false) + async public 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 e = -1, bool play = false, bool rjson = false, bool checksearch = false) { await UpdateService.ConnectAsync(host); @@ -37,6 +37,18 @@ namespace CikavaIdeya.Controllers var invoke = new CikavaIdeyaInvoke(init, hybridCache, OnLog, proxyManager); + if (checksearch) + { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError("cikavaideya", proxyManager); + + var episodesInfo = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial == 0); + if (episodesInfo != null && episodesInfo.Count > 0) + return Content("data-json=", "text/plain; charset=utf-8"); + + return OnError("cikavaideya", proxyManager); + } + var episodesInfo = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial == 0); if (episodesInfo == null) return Content("CikavaIdeya", "text/html; charset=utf-8"); diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 810e86e..c320836 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -27,7 +27,12 @@ namespace Makhno 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="); + { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError(); + + return Content("data-json=", "text/plain; charset=utf-8"); + } await UpdateService.ConnectAsync(host); diff --git a/Mikai/Controller.cs b/Mikai/Controller.cs index 90fdaef..d0384d5 100644 --- a/Mikai/Controller.cs +++ b/Mikai/Controller.cs @@ -24,7 +24,7 @@ namespace Mikai.Controllers [HttpGet] [Route("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) + 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); @@ -33,6 +33,19 @@ namespace Mikai.Controllers return Forbid(); var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager); + + if (checksearch) + { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError("mikai", _proxyManager); + + var searchResults = await invoke.Search(title, original_title, year); + if (searchResults != null && searchResults.Count > 0) + return Content("data-json=", "text/plain; charset=utf-8"); + + return OnError("mikai", _proxyManager); + } + 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); diff --git a/StarLight/Controller.cs b/StarLight/Controller.cs index 0dadfb4..cc15189 100644 --- a/StarLight/Controller.cs +++ b/StarLight/Controller.cs @@ -25,7 +25,7 @@ namespace StarLight.Controllers [HttpGet] [Route("starlight")] - async public 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, int s = -1, bool rjson = false, string href = null) + async public 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, int s = -1, bool rjson = false, string href = null, bool checksearch = false) { await UpdateService.ConnectAsync(host); @@ -35,6 +35,18 @@ namespace StarLight.Controllers var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager); + if (checksearch) + { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError("starlight", proxyManager); + + var searchResults = await invoke.Search(title, original_title); + if (searchResults != null && searchResults.Count > 0) + return Content("data-json=", "text/plain; charset=utf-8"); + + return OnError("starlight", proxyManager); + } + string itemUrl = href; if (string.IsNullOrEmpty(itemUrl)) { diff --git a/UAKino/Controller.cs b/UAKino/Controller.cs index f10ea6e..3df6ad4 100644 --- a/UAKino/Controller.cs +++ b/UAKino/Controller.cs @@ -23,7 +23,7 @@ namespace UAKino.Controllers [HttpGet] [Route("uakino")] - async public 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, bool rjson = false, string href = null) + async public 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, bool rjson = false, string href = null, bool checksearch = false) { await UpdateService.ConnectAsync(host); @@ -33,6 +33,18 @@ namespace UAKino.Controllers var invoke = new UAKinoInvoke(init, hybridCache, OnLog, proxyManager); + if (checksearch) + { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError("uakino", proxyManager); + + var searchResults = await invoke.Search(title, original_title, serial); + if (searchResults != null && searchResults.Count > 0) + return Content("data-json=", "text/plain; charset=utf-8"); + + return OnError("uakino", proxyManager); + } + string itemUrl = href; if (string.IsNullOrEmpty(itemUrl)) { diff --git a/UaTUT/Controller.cs b/UaTUT/Controller.cs index 13dac3a..ecb64f3 100644 --- a/UaTUT/Controller.cs +++ b/UaTUT/Controller.cs @@ -25,7 +25,7 @@ namespace UaTUT } [HttpGet] - async public 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) + async public 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) { await UpdateService.ConnectAsync(host); @@ -45,6 +45,17 @@ namespace UaTUT return await invoke.Search(original_title ?? title, imdb_id); }); + if (checksearch) + { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError(); + + if (searchResults != null && searchResults.Any()) + return Content("data-json=", "text/plain; charset=utf-8"); + + return OnError(); + } + if (searchResults == null || !searchResults.Any()) { OnLog("UaTUT: No search results found"); diff --git a/Uaflix/Controller.cs b/Uaflix/Controller.cs index 4ca2558..7512907 100644 --- a/Uaflix/Controller.cs +++ b/Uaflix/Controller.cs @@ -47,6 +47,9 @@ namespace Uaflix.Controllers // Обробка параметра checksearch - повертаємо спеціальну відповідь для валідації if (checksearch) { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError("uaflix", proxyManager); + try { string filmTitle = !string.IsNullOrEmpty(title) ? title : original_title; diff --git a/Unimay/Controllers/Controller.cs b/Unimay/Controllers/Controller.cs index 0c692f9..5242090 100644 --- a/Unimay/Controllers/Controller.cs +++ b/Unimay/Controllers/Controller.cs @@ -23,7 +23,7 @@ namespace Unimay.Controllers [HttpGet] [Route("unimay")] - async public ValueTask Index(string title, string original_title, string code, int serial = -1, int s = -1, int e = -1, bool play = false, bool rjson = false) + async public ValueTask Index(string title, string original_title, string code, int serial = -1, int s = -1, int e = -1, bool play = false, bool rjson = false, bool checksearch = false) { await UpdateService.ConnectAsync(host); @@ -33,6 +33,18 @@ namespace Unimay.Controllers var invoke = new UnimayInvoke(init, hybridCache, OnLog, proxyManager); + if (checksearch) + { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError("unimay"); + + var searchResults = await invoke.Search(title, original_title, serial); + if (searchResults?.Content != null && searchResults.Content.Count > 0) + return Content("data-json=", "text/plain; charset=utf-8"); + + return OnError("unimay"); + } + if (!string.IsNullOrEmpty(code)) { // Fetch release details From 630c91e53b8de1d0e080d90ddc83583e312200b6 Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Fri, 6 Feb 2026 17:18:04 +0200 Subject: [PATCH 36/37] refactor(controllers): rename search result variables for consistency Rename variables used to store search results across multiple controllers to follow a consistent naming pattern (checkSeasons, checkEpisodes, checkResults) instead of generic names like seasons, episodesInfo, and searchResults. This improves code readability and maintains uniform variable naming across the codebase. --- AnimeON/Controller.cs | 4 ++-- CikavaIdeya/Controller.cs | 4 ++-- Mikai/Controller.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/AnimeON/Controller.cs b/AnimeON/Controller.cs index a4307db..b125a9d 100644 --- a/AnimeON/Controller.cs +++ b/AnimeON/Controller.cs @@ -43,8 +43,8 @@ namespace AnimeON.Controllers if (AppInit.conf?.online?.checkOnlineSearch != true) return OnError("animeon", proxyManager); - var seasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial); - if (seasons != null && seasons.Count > 0) + 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("animeon", proxyManager); diff --git a/CikavaIdeya/Controller.cs b/CikavaIdeya/Controller.cs index e0612dd..b1fcf63 100644 --- a/CikavaIdeya/Controller.cs +++ b/CikavaIdeya/Controller.cs @@ -42,8 +42,8 @@ namespace CikavaIdeya.Controllers if (AppInit.conf?.online?.checkOnlineSearch != true) return OnError("cikavaideya", proxyManager); - var episodesInfo = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial == 0); - if (episodesInfo != null && episodesInfo.Count > 0) + var checkEpisodes = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial == 0); + if (checkEpisodes != null && checkEpisodes.Count > 0) return Content("data-json=", "text/plain; charset=utf-8"); return OnError("cikavaideya", proxyManager); diff --git a/Mikai/Controller.cs b/Mikai/Controller.cs index d0384d5..87489db 100644 --- a/Mikai/Controller.cs +++ b/Mikai/Controller.cs @@ -39,8 +39,8 @@ namespace Mikai.Controllers if (AppInit.conf?.online?.checkOnlineSearch != true) return OnError("mikai", _proxyManager); - var searchResults = await invoke.Search(title, original_title, year); - if (searchResults != null && searchResults.Count > 0) + 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", _proxyManager); From d91629079a07e7bb8088e1cad6583490f50b71dd Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Fri, 6 Feb 2026 17:25:12 +0200 Subject: [PATCH 37/37] chore(version): bump versions across multiple streaming service modules Update version numbers for AnimeON, Bamboo, CikavaIdeya, Makhno, Mikai, UAKino, UaTUT, Uaflix, and Unimay modules to reflect latest releases. Note that CikavaIdeya and UAKino have unusual binary-looking version numbers that appear intentional. --- AnimeON/ModInit.cs | 2 +- Bamboo/ModInit.cs | 2 +- CikavaIdeya/ModInit.cs | 2 +- Makhno/ModInit.cs | 2 +- Mikai/ModInit.cs | 2 +- UAKino/ModInit.cs | 2 +- UaTUT/ModInit.cs | 2 +- Uaflix/ModInit.cs | 2 +- Unimay/ModInit.cs | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/AnimeON/ModInit.cs b/AnimeON/ModInit.cs index fdb23df..29c135c 100644 --- a/AnimeON/ModInit.cs +++ b/AnimeON/ModInit.cs @@ -25,7 +25,7 @@ namespace AnimeON { public class ModInit { - public static double Version => 3.3; + public static double Version => 3.4; public static OnlinesSettings AnimeON; public static bool ApnHostProvided; diff --git a/Bamboo/ModInit.cs b/Bamboo/ModInit.cs index 9da828b..658479d 100644 --- a/Bamboo/ModInit.cs +++ b/Bamboo/ModInit.cs @@ -24,7 +24,7 @@ namespace Bamboo { public class ModInit { - public static double Version => 3.4; + public static double Version => 3.5; public static OnlinesSettings Bamboo; public static bool ApnHostProvided; diff --git a/CikavaIdeya/ModInit.cs b/CikavaIdeya/ModInit.cs index e411b1b..d436297 100644 --- a/CikavaIdeya/ModInit.cs +++ b/CikavaIdeya/ModInit.cs @@ -30,7 +30,7 @@ namespace CikavaIdeya { public class ModInit { - public static double Version => 3.2; + public static double Version => 010100100100100101010000; public static OnlinesSettings CikavaIdeya; public static bool ApnHostProvided; diff --git a/Makhno/ModInit.cs b/Makhno/ModInit.cs index bc5b7a0..1e173c8 100644 --- a/Makhno/ModInit.cs +++ b/Makhno/ModInit.cs @@ -23,7 +23,7 @@ namespace Makhno { public class ModInit { - public static double Version => 1.6; + public static double Version => 1.7; public static OnlinesSettings Makhno; public static bool ApnHostProvided; diff --git a/Mikai/ModInit.cs b/Mikai/ModInit.cs index c9ef993..e3cba11 100644 --- a/Mikai/ModInit.cs +++ b/Mikai/ModInit.cs @@ -24,7 +24,7 @@ namespace Mikai { public class ModInit { - public static double Version => 3.4; + public static double Version => 3.5; public static OnlinesSettings Mikai; public static bool ApnHostProvided; diff --git a/UAKino/ModInit.cs b/UAKino/ModInit.cs index 17dd74b..4ddd5a5 100644 --- a/UAKino/ModInit.cs +++ b/UAKino/ModInit.cs @@ -23,7 +23,7 @@ namespace UAKino { public class ModInit { - public static double Version => 3.3; + public static double Version => 010100100100100101010000; public static OnlinesSettings UAKino; public static bool ApnHostProvided; diff --git a/UaTUT/ModInit.cs b/UaTUT/ModInit.cs index 8813e0b..db955f9 100644 --- a/UaTUT/ModInit.cs +++ b/UaTUT/ModInit.cs @@ -24,7 +24,7 @@ namespace UaTUT { public class ModInit { - public static double Version => 3.4; + public static double Version => 3.5; public static OnlinesSettings UaTUT; public static bool ApnHostProvided; diff --git a/Uaflix/ModInit.cs b/Uaflix/ModInit.cs index 370547b..87b62c1 100644 --- a/Uaflix/ModInit.cs +++ b/Uaflix/ModInit.cs @@ -25,7 +25,7 @@ namespace Uaflix { public class ModInit { - public static double Version => 3.4; + public static double Version => 3.5; public static OnlinesSettings UaFlix; public static bool ApnHostProvided; diff --git a/Unimay/ModInit.cs b/Unimay/ModInit.cs index 2984448..4f15197 100644 --- a/Unimay/ModInit.cs +++ b/Unimay/ModInit.cs @@ -30,7 +30,7 @@ namespace Unimay { public class ModInit { - public static double Version => 3.2; + public static double Version => 3.3; public static OnlinesSettings Unimay;