lampac-ukraine/AnimeON/Controller.cs
baliasnyifeliks 3576932089 feat(animeon): enhance search for serials and improve voice display
Add serial parameter to Search method to enable enhanced search logic for
series content. When serial flag is set, perform additional search using
English title to find more results. Improve voice selection by properly
handling display names with fallbacks to key or default label.
2026-02-01 18:05:28 +02:00

396 lines
18 KiB
C#

using System.Text.Json;
using Shared.Engine;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Web;
using System.Linq;
using Shared;
using Shared.Models.Templates;
using AnimeON.Models;
using System.Text.RegularExpressions;
using System.Text;
using Shared.Models.Online.Settings;
using Shared.Models;
using HtmlAgilityPack;
namespace AnimeON.Controllers
{
public class Controller : BaseOnlineController
{
ProxyManager proxyManager;
public Controller() : base(ModInit.Settings)
{
proxyManager = new ProxyManager(ModInit.AnimeON);
}
[HttpGet]
[Route("animeon")]
async public Task<ActionResult> 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)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.AnimeON);
if (!init.enable)
return Forbid();
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, 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);
OnLog($"AnimeON: search results = {seasons?.Count ?? 0}");
if (seasons == null || seasons.Count == 0)
return OnError("animeon", proxyManager);
// [Refactoring] Використовується агрегована структура (AggregateSerialStructure) — попередній збір allOptions не потрібний
// [Refactoring] Перевірка allOptions видалена — використовується перевірка структури озвучок нижче
if (serial == 1)
{
if (s == -1) // Крок 1: Вибір аніме (як сезони)
{
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)
{
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={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: Вибір озвучки та епізодів
{
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 = selected.Anime;
int selectedSeasonNumber = selected.SeasonNumber;
var structure = await invoke.AggregateSerialStructure(selectedAnime.Id, selectedSeasonNumber);
if (structure == null || !structure.Voices.Any())
return OnError("animeon", proxyManager);
OnLog($"AnimeON: voices found = {structure.Voices.Count}");
var voiceItems = structure.Voices
.Select(v =>
{
string display = v.Value?.DisplayName;
if (string.IsNullOrWhiteSpace(display))
display = v.Key;
if (string.IsNullOrWhiteSpace(display))
display = "Озвучка";
return new { Key = v.Key, Display = display };
})
.ToList();
// Автовибір першої озвучки якщо t не задано
if (string.IsNullOrEmpty(t))
t = voiceItems.First().Key;
// Формуємо список озвучок
var voice_tpl = new VoiceTpl();
foreach (var voice in voiceItems)
{
string voiceLink = $"{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={s}&t={HttpUtility.UrlEncode(voice.Key)}";
bool isActive = voice.Key == t;
voice_tpl.Append(voice.Display, isActive, voiceLink);
}
// Перевірка вибраної озвучки
if (!structure.Voices.ContainsKey(t))
return OnError("animeon", proxyManager);
var episode_tpl = new EpisodeTpl();
var selectedVoiceInfo = structure.Voices[t];
// Формуємо епізоди для вибраної озвучки
foreach (var ep in selectedVoiceInfo.Episodes.OrderBy(e => e.Number))
{
string episodeName = !string.IsNullOrEmpty(ep.Title) ? ep.Title : $"Епізод {ep.Number}";
string seasonStr = selectedSeasonNumber.ToString();
string episodeStr = ep.Number.ToString();
string streamLink = !string.IsNullOrEmpty(ep.Hls) ? ep.Hls : ep.VideoUrl;
bool needsResolve = selectedVoiceInfo.PlayerType == "moon" || selectedVoiceInfo.PlayerType == "ashdi";
if (string.IsNullOrEmpty(streamLink) && ep.EpisodeId > 0)
{
string callUrl = $"{host}/animeon/play?episode_id={ep.EpisodeId}";
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
continue;
}
if (string.IsNullOrEmpty(streamLink))
continue;
if (needsResolve || streamLink.Contains("moonanime.art") || streamLink.Contains("ashdi.vip/vod"))
{
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
}
else
{
string playUrl = HostStreamProxy(init, accsArgs(streamLink));
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, playUrl);
}
}
// Повертаємо озвучки + епізоди разом
OnLog($"AnimeON: return episodes count={selectedVoiceInfo.Episodes.Count} for voice='{t}' season={selectedAnime.Season}");
if (rjson)
return Content(episode_tpl.ToJson(voice_tpl), "application/json; charset=utf-8");
return Content(voice_tpl.ToHtml() + episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
}
else // Фільм
{
var firstAnime = seasons.FirstOrDefault();
if (firstAnime == null)
return OnError("animeon", proxyManager);
var fundubs = await invoke.GetFundubs(firstAnime.Id);
OnLog($"AnimeON: movie fundubs count = {fundubs?.Count ?? 0}");
if (fundubs == null || fundubs.Count == 0)
return OnError("animeon", proxyManager);
var tpl = new MovieTpl(title, original_title);
foreach (var fundub in fundubs)
{
if (fundub?.Fundub == null || fundub.Player == null || fundub.Player.Count == 0)
continue;
foreach (var player in fundub.Player)
{
var episodesData = await invoke.GetEpisodes(firstAnime.Id, player.Id, fundub.Fundub.Id);
if (episodesData == null || episodesData.Episodes == null || episodesData.Episodes.Count == 0)
continue;
var firstEp = episodesData.Episodes.FirstOrDefault();
if (firstEp == null)
continue;
string streamLink = !string.IsNullOrEmpty(firstEp.Hls) ? firstEp.Hls : firstEp.VideoUrl;
if (string.IsNullOrEmpty(streamLink))
continue;
string translationName = $"[{player.Name}] {fundub.Fundub.Name}";
bool needsResolve = player.Name?.ToLower() == "moon" || player.Name?.ToLower() == "ashdi";
if (needsResolve || streamLink.Contains("moonanime.art/iframe/") || streamLink.Contains("ashdi.vip/vod"))
{
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";
tpl.Append(translationName, accsArgs(callUrl), "call");
}
else
{
tpl.Append(translationName, HostStreamProxy(init, accsArgs(streamLink)));
}
}
}
// Якщо не зібрали жодної опції — повертаємо помилку
if (tpl.data == null || tpl.data.Count == 0)
return OnError("animeon", proxyManager);
OnLog("AnimeON: return movie options");
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8");
}
}
async Task<List<FundubModel>> GetFundubs(OnlinesSettings init, int animeId)
{
string fundubsUrl = $"{init.host}/api/player/{animeId}/translations";
string fundubsJson = await Http.Get(fundubsUrl, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
if (string.IsNullOrEmpty(fundubsJson))
return null;
var fundubsResponse = JsonSerializer.Deserialize<FundubsResponseModel>(fundubsJson);
if (fundubsResponse?.Translations == null || fundubsResponse.Translations.Count == 0)
return null;
var fundubs = new List<FundubModel>();
foreach (var translation in fundubsResponse.Translations)
{
var fundubModel = new FundubModel
{
Fundub = translation.Translation,
Player = translation.Player
};
fundubs.Add(fundubModel);
}
return fundubs;
}
async Task<EpisodeModel> GetEpisodes(OnlinesSettings init, int animeId, int playerId, int fundubId)
{
string episodesUrl = $"{init.host}/api/player/{animeId}/episodes?take=100&skip=-1&playerId={playerId}&translationId={fundubId}";
string episodesJson = await Http.Get(episodesUrl, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
if (string.IsNullOrEmpty(episodesJson))
return null;
return JsonSerializer.Deserialize<EpisodeModel>(episodesJson);
}
async ValueTask<List<SearchModel>> search(OnlinesSettings init, string imdb_id, long kinopoisk_id, string title, string original_title, int year)
{
string memKey = $"AnimeON:search:{kinopoisk_id}:{imdb_id}";
if (hybridCache.TryGetValue(memKey, out List<SearchModel> res))
return res;
try
{
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) };
async Task<List<SearchModel>> FindAnime(string query)
{
if (string.IsNullOrEmpty(query))
return null;
string searchUrl = $"{init.host}/api/anime/search?text={HttpUtility.UrlEncode(query)}";
string searchJson = await Http.Get(searchUrl, headers: headers);
if (string.IsNullOrEmpty(searchJson))
return null;
var searchResponse = JsonSerializer.Deserialize<SearchResponseModel>(searchJson);
return searchResponse?.Result;
}
var searchResults = await FindAnime(title) ?? await FindAnime(original_title);
if (searchResults == null)
return null;
if (!string.IsNullOrEmpty(imdb_id))
{
var seasons = searchResults.Where(a => a.ImdbId == imdb_id).ToList();
if (seasons.Count > 0)
{
hybridCache.Set(memKey, seasons, cacheTime(5));
return seasons;
}
}
// Fallback to first result if no imdb match
var firstResult = searchResults.FirstOrDefault();
if (firstResult != null)
{
var list = new List<SearchModel> { firstResult };
hybridCache.Set(memKey, list, cacheTime(5));
return list;
}
return null;
}
catch (Exception ex)
{
OnLog($"AnimeON error: {ex.Message}");
}
return null;
}
[HttpGet("animeon/play")]
public async Task<ActionResult> Play(string url, int episode_id = 0, string title = null)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.AnimeON);
if (!init.enable)
return Forbid();
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager);
OnLog($"AnimeON Play: url={url}, episode_id={episode_id}");
string streamLink = null;
if (episode_id > 0)
{
streamLink = await invoke.ResolveEpisodeStream(episode_id);
}
else if (!string.IsNullOrEmpty(url))
{
streamLink = await invoke.ResolveVideoUrl(url);
}
else
{
OnLog("AnimeON Play: empty url");
return OnError("animeon", proxyManager);
}
if (string.IsNullOrEmpty(streamLink))
{
OnLog("AnimeON Play: cannot extract stream");
return OnError("animeon", proxyManager);
}
List<HeadersModel> streamHeaders = null;
bool forceProxy = false;
if (streamLink.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase))
{
streamHeaders = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://ashdi.vip/")
};
forceProxy = true;
}
string streamUrl = BuildStreamUrl(init, streamLink, streamHeaders, forceProxy);
string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? string.Empty}\"}}";
OnLog("AnimeON Play: return call JSON");
return UpdateService.Validate(Content(jsonResult, "application/json; charset=utf-8"));
}
string BuildStreamUrl(OnlinesSettings init, string streamLink, List<HeadersModel> headers, bool forceProxy)
{
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, headers: headers, force_streamproxy: forceProxy);
}
return HostStreamProxy(init, link, headers: headers, force_streamproxy: forceProxy);
}
}
}