From ff320a97f97934625d4cf400ca7da1fbe57963dd Mon Sep 17 00:00:00 2001 From: baliasnyifeliks Date: Sun, 1 Feb 2026 17:38:37 +0200 Subject: [PATCH] feat(animeon, mikai): add multi-season support for anime controllers Implement comprehensive multi-season handling for AnimeON and Mikai controllers to properly display and navigate anime series with multiple seasons. For AnimeON: - Fix season numbering to use actual season numbers instead of array indices - Group seasons by SeasonNumber property with fallback to index-based numbering - Update season selection links to use correct season identifiers For Mikai: - Add relation fetching to discover sequels, prequels, and related anime - Implement season detail collection and ordering by release date - Refactor voice building to support multiple seasons per voice actor - Update season selection UI to display all available seasons - Add MikaiRelation and MikaiRelationAnime models for API data handling --- AnimeON/Controller.cs | 50 +++++++--- Mikai/Controller.cs | 185 +++++++++++++++++++++++++++--------- Mikai/Models/MikaiModels.cs | 27 ++++++ 3 files changed, 206 insertions(+), 56 deletions(-) diff --git a/AnimeON/Controller.cs b/AnimeON/Controller.cs index 286a54f..0c1b7df 100644 --- a/AnimeON/Controller.cs +++ b/AnimeON/Controller.cs @@ -52,24 +52,52 @@ namespace AnimeON.Controllers { if (s == -1) // Крок 1: Вибір аніме (як сезони) { - var season_tpl = new SeasonTpl(seasons.Count); - for (int i = 0; i < seasons.Count; i++) + var seasonItems = seasons + .Select((anime, index) => new + { + Anime = anime, + Index = index, + SeasonNumber = anime.Season > 0 ? anime.Season : index + 1 + }) + .GroupBy(x => x.SeasonNumber) + .Select(g => g.First()) + .OrderBy(x => x.SeasonNumber) + .ToList(); + + var season_tpl = new SeasonTpl(seasonItems.Count); + foreach (var item in seasonItems) { - var anime = seasons[i]; - string seasonName = anime.Season.ToString(); - string link = $"{host}/animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={i}"; - season_tpl.Append(seasonName, link, anime.Season.ToString()); + string seasonName = item.SeasonNumber.ToString(); + string link = $"{host}/animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={item.SeasonNumber}"; + season_tpl.Append(seasonName, link, seasonName); } - OnLog($"AnimeON: return seasons count={seasons.Count}"); + OnLog($"AnimeON: return seasons count={seasonItems.Count}"); return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8"); } else // Крок 2/3: Вибір озвучки та епізодів { - if (s >= seasons.Count) + var seasonItems = seasons + .Select((anime, index) => new + { + Anime = anime, + Index = index, + SeasonNumber = anime.Season > 0 ? anime.Season : index + 1 + }) + .GroupBy(x => x.SeasonNumber) + .Select(g => g.First()) + .OrderBy(x => x.SeasonNumber) + .ToList(); + + var selected = seasonItems.FirstOrDefault(x => x.SeasonNumber == s); + if (selected == null && s >= 0 && s < seasons.Count) + selected = new { Anime = seasons[s], Index = s, SeasonNumber = seasons[s].Season > 0 ? seasons[s].Season : s + 1 }; + + if (selected == null) return OnError("animeon", proxyManager); - var selectedAnime = seasons[s]; - var structure = await invoke.AggregateSerialStructure(selectedAnime.Id, selectedAnime.Season); + var selectedAnime = selected.Anime; + int selectedSeasonNumber = selected.SeasonNumber; + var structure = await invoke.AggregateSerialStructure(selectedAnime.Id, selectedSeasonNumber); if (structure == null || !structure.Voices.Any()) return OnError("animeon", proxyManager); @@ -98,7 +126,7 @@ namespace AnimeON.Controllers foreach (var ep in selectedVoiceInfo.Episodes.OrderBy(e => e.Number)) { string episodeName = !string.IsNullOrEmpty(ep.Title) ? ep.Title : $"Епізод {ep.Number}"; - string seasonStr = selectedAnime.Season.ToString(); + string seasonStr = selectedSeasonNumber.ToString(); string episodeStr = ep.Number.ToString(); string streamLink = !string.IsNullOrEmpty(ep.Hls) ? ep.Hls : ep.VideoUrl; diff --git a/Mikai/Controller.cs b/Mikai/Controller.cs index 3fcd339..c664336 100644 --- a/Mikai/Controller.cs +++ b/Mikai/Controller.cs @@ -48,7 +48,8 @@ namespace Mikai.Controllers return OnError("mikai", _proxyManager); bool isSerial = serial == 1 || (serial == -1 && !string.Equals(details.Format, "movie", StringComparison.OrdinalIgnoreCase)); - var voices = BuildVoices(details); + var seasonDetails = await CollectSeasonDetails(details, invoke); + var voices = BuildVoices(seasonDetails); if (voices.Count == 0) return OnError("mikai", _proxyManager); @@ -56,12 +57,23 @@ namespace Mikai.Controllers if (isSerial) { - const int seasonNumber = 1; + var seasonNumbers = voices.Values + .SelectMany(v => v.Seasons.Keys) + .Distinct() + .OrderBy(n => n) + .ToList(); + + if (seasonNumbers.Count == 0) + return OnError("mikai", _proxyManager); + if (s == -1) { - var seasonTpl = new SeasonTpl(1); - 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}"; - seasonTpl.Append($"{seasonNumber}", link, seasonNumber.ToString()); + var seasonTpl = new SeasonTpl(seasonNumbers.Count); + 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}"; + seasonTpl.Append($"{seasonNumber}", link, seasonNumber.ToString()); + } return rjson ? Content(seasonTpl.ToJson(), "application/json; charset=utf-8") @@ -178,65 +190,148 @@ namespace Mikai.Controllers return UpdateService.Validate(Content(jsonResult, "application/json; charset=utf-8")); } - private Dictionary BuildVoices(MikaiAnime details) + private async Task> CollectSeasonDetails(MikaiAnime details, MikaiInvoke invoke) { - var voices = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (details?.Players == null) - return voices; + var seasonDetails = new List(); + if (details == null) + return seasonDetails; - int totalProviders = details.Players.Sum(p => p?.Providers?.Count ?? 0); + seasonDetails.Add(details); - foreach (var player in details.Players) + if (details.Relations == null || details.Relations.Count == 0) + return seasonDetails; + + var relationIds = details.Relations + .Where(r => ShouldIncludeRelation(r?.RelationType)) + .Select(r => r?.Anime?.Id ?? 0) + .Where(id => id > 0 && id != details.Id) + .Distinct() + .ToList(); + + foreach (var relationId in relationIds) { - if (player?.Providers == null || player.Providers.Count == 0) + var relationDetails = await invoke.GetDetails(relationId); + if (relationDetails?.Players == null || relationDetails.Players.Count == 0) continue; - string teamName = player.Team?.Name; - if (string.IsNullOrWhiteSpace(teamName)) - teamName = "Озвучка"; + seasonDetails.Add(relationDetails); + } - string baseName = player.IsSubs ? $"{teamName} (Субтитри)" : teamName; + return OrderSeasonDetails(seasonDetails); + } - foreach (var provider in player.Providers) + private static bool ShouldIncludeRelation(string relationType) + { + if (string.IsNullOrWhiteSpace(relationType)) + return false; + + return relationType.Equals("other", StringComparison.OrdinalIgnoreCase) || + relationType.Equals("parent", StringComparison.OrdinalIgnoreCase) || + relationType.Equals("sequel", StringComparison.OrdinalIgnoreCase) || + relationType.Equals("prequel", StringComparison.OrdinalIgnoreCase); + } + + private static List OrderSeasonDetails(List seasonDetails) + { + return seasonDetails + .Where(d => d != null) + .GroupBy(d => d.Id) + .Select(g => g.First()) + .OrderBy(d => d.Year > 0 ? d.Year : int.MaxValue) + .ThenBy(d => { - if (provider?.Episodes == null || provider.Episodes.Count == 0) + if (DateTime.TryParse(d.StartDate, out var parsed)) + return parsed; + + return DateTime.MaxValue; + }) + .ThenBy(d => d.Id) + .ToList(); + } + + private Dictionary BuildVoices(List seasonDetails) + { + var voices = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (seasonDetails == null || seasonDetails.Count == 0) + return voices; + + var voiceKeyMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + int seasonNumber = 1; + + foreach (var details in seasonDetails) + { + if (details?.Players == null || details.Players.Count == 0) + { + seasonNumber++; + continue; + } + + int totalProviders = details.Players.Sum(p => p?.Providers?.Count ?? 0); + + foreach (var player in details.Players) + { + if (player?.Providers == null || player.Providers.Count == 0) continue; - string displayName = baseName; - if (totalProviders > 1 && !string.IsNullOrWhiteSpace(provider.Name)) - displayName = $"[{provider.Name}] {displayName}"; + string teamName = player.Team?.Name; + if (string.IsNullOrWhiteSpace(teamName)) + teamName = "Озвучка"; - displayName = EnsureUniqueName(voices, displayName); + string baseName = player.IsSubs ? $"{teamName} (Субтитри)" : teamName; - var voice = new MikaiVoiceInfo + int providerIndex = 0; + foreach (var provider in player.Providers) { - DisplayName = displayName, - ProviderName = provider.Name, - IsSubs = player.IsSubs - }; - - var episodes = new List(); - int fallbackIndex = 1; - foreach (var ep in provider.Episodes.OrderBy(e => e.Number)) - { - if (string.IsNullOrWhiteSpace(ep.PlayLink)) + providerIndex++; + if (provider?.Episodes == null || provider.Episodes.Count == 0) continue; - int number = ep.Number > 0 ? ep.Number : fallbackIndex++; - episodes.Add(new MikaiEpisodeInfo + string displayName = baseName; + if (totalProviders > 1 && !string.IsNullOrWhiteSpace(provider.Name)) + displayName = $"[{provider.Name}] {displayName}"; + + string providerKey = string.IsNullOrWhiteSpace(provider.Name) ? $"provider-{providerIndex}" : provider.Name; + string voiceKey = $"{providerKey}|{teamName}|{player.IsSubs}"; + + if (!voiceKeyMap.TryGetValue(voiceKey, out var voiceName)) { - Number = number, - Title = $"Епізод {number}", - Url = ep.PlayLink - }); + displayName = EnsureUniqueName(voices, displayName); + voiceKeyMap[voiceKey] = displayName; + voices[displayName] = new MikaiVoiceInfo + { + DisplayName = displayName, + ProviderName = provider.Name, + IsSubs = player.IsSubs + }; + voiceName = displayName; + } + + var voice = voices[voiceName]; + + var episodes = new List(); + int fallbackIndex = 1; + foreach (var ep in provider.Episodes.OrderBy(e => e.Number)) + { + if (string.IsNullOrWhiteSpace(ep.PlayLink)) + continue; + + int number = ep.Number > 0 ? ep.Number : fallbackIndex++; + episodes.Add(new MikaiEpisodeInfo + { + Number = number, + Title = $"Епізод {number}", + Url = ep.PlayLink + }); + } + + if (episodes.Count == 0) + continue; + + voice.Seasons[seasonNumber] = episodes; } - - if (episodes.Count == 0) - continue; - - voice.Seasons[1] = episodes; - voices[displayName] = voice; } + + seasonNumber++; } return voices; diff --git a/Mikai/Models/MikaiModels.cs b/Mikai/Models/MikaiModels.cs index b1e6c6f..5d17c35 100644 --- a/Mikai/Models/MikaiModels.cs +++ b/Mikai/Models/MikaiModels.cs @@ -61,6 +61,9 @@ namespace Mikai.Models [JsonPropertyName("players")] public List Players { get; set; } + + [JsonPropertyName("relations")] + public List Relations { get; set; } } public class MikaiMedia @@ -206,4 +209,28 @@ namespace Mikai.Models [JsonPropertyName("playLink")] public string PlayLink { get; set; } } + + public class MikaiRelation + { + [JsonPropertyName("relationType")] + public string RelationType { get; set; } + + [JsonPropertyName("anime")] + public MikaiRelationAnime Anime { get; set; } + } + + public class MikaiRelationAnime + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("slug")] + public string Slug { get; set; } + + [JsonPropertyName("media")] + public MikaiMedia Media { get; set; } + + [JsonPropertyName("details")] + public MikaiDetails Details { get; set; } + } }