diff --git a/AnimeON/AnimeON.csproj b/AnimeON/AnimeON.csproj new file mode 100644 index 0000000..c26a806 --- /dev/null +++ b/AnimeON/AnimeON.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + library + true + + + + + ..\..\Shared.dll + + + + \ No newline at end of file diff --git a/AnimeON/Controller.cs b/AnimeON/Controller.cs new file mode 100644 index 0000000..635a7bf --- /dev/null +++ b/AnimeON/Controller.cs @@ -0,0 +1,192 @@ +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 Shared.Models.Online.Settings; +using Shared.Models; +using HtmlAgilityPack; + +namespace AnimeON.Controllers +{ + public class Controller : BaseOnlineController + { + ProxyManager proxyManager; + + public Controller() + { + proxyManager = new ProxyManager(ModInit.AnimeON); + } + + [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) + { + var init = await loadKit(ModInit.AnimeON); + if (!init.enable) + return Forbid(); + + var seasons = await search(init, imdb_id, kinopoisk_id, title, original_title, year); + if (seasons == null || seasons.Count == 0) + return Content("AnimeON", "text/html; charset=utf-8"); + + var allOptions = new List<(SearchModel season, FundubModel fundub, Player player)>(); + foreach (var season in seasons) + { + var fundubs = await GetFundubs(init, season.Id); + if (fundubs != null) + { + foreach (var fundub in fundubs) + { + foreach (var player in fundub.Player) + { + allOptions.Add((season, fundub, player)); + } + } + } + } + + if (allOptions.Count == 0) + return Content("AnimeON", "text/html; charset=utf-8"); + + if (serial == 1) + { + if (s == -1) // Выбор сезона/озвучки + { + var season_tpl = new SeasonTpl(allOptions.Count); + for (int i = 0; i < allOptions.Count; i++) + { + var item = allOptions[i]; + string translationName = $"[{item.player.Name}|S{item.season.Season}] {item.fundub.Fundub.Name}"; + 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(translationName, link, $"{i}"); + } + return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8"); + } + else // Вывод эпизодов + { + if (s >= allOptions.Count) + return Content("AnimeON", "text/html; charset=utf-8"); + + var selected = allOptions[s]; + var episodesData = await GetEpisodes(init, selected.season.Id, selected.player.Id, selected.fundub.Fundub.Id); + if (episodesData == null || episodesData.Episodes == null) + return Content("AnimeON", "text/html; charset=utf-8"); + + var movie_tpl = new MovieTpl(title, original_title, episodesData.Episodes.Count); + foreach (var ep in episodesData.Episodes.OrderBy(e => e.EpisodeNum)) + { + var streamquality = new StreamQualityTpl(); + string streamLink = !string.IsNullOrEmpty(ep.Hls) ? ep.Hls : ep.VideoUrl; + streamquality.Append(HostStreamProxy(init, streamLink), "hls"); + movie_tpl.Append(string.IsNullOrEmpty(ep.Name) ? $"Серія {ep.EpisodeNum}" : ep.Name, streamquality.Firts().link, streamquality: streamquality); + } + return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8"); + } + } + else // Фильм + { + var tpl = new MovieTpl(title, original_title, allOptions.Count); + foreach (var item in allOptions) + { + var episodesData = await GetEpisodes(init, item.season.Id, item.player.Id, item.fundub.Fundub.Id); + if (episodesData == null || episodesData.Episodes == null || episodesData.Episodes.Count == 0) + continue; + + string translationName = $"[{item.player.Name}] {item.fundub.Fundub.Name}"; + var streamquality = new StreamQualityTpl(); + var firstEp = episodesData.Episodes.First(); + string streamLink = !string.IsNullOrEmpty(firstEp.Hls) ? firstEp.Hls : firstEp.VideoUrl; + streamquality.Append(HostStreamProxy(init, streamLink), "hls"); + tpl.Append(translationName, streamquality.Firts().link, streamquality: streamquality); + } + return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8"); + } + } + + async Task> GetFundubs(OnlinesSettings init, int animeId) + { + string fundubsUrl = $"{init.host}/api/player/fundubs/{animeId}"; + string fundubsJson = await Http.Get(fundubsUrl, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) }); + if (string.IsNullOrEmpty(fundubsJson)) + return null; + + var fundubsResponse = JsonSerializer.Deserialize(fundubsJson); + return fundubsResponse?.FunDubs; + } + + async Task GetEpisodes(OnlinesSettings init, int animeId, int playerId, int fundubId) + { + string episodesUrl = $"{init.host}/api/player/episodes/{animeId}?take=100&skip=-1&playerId={playerId}&fundubId={fundubId}"; + string episodesJson = await Http.Get(episodesUrl, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) }); + if (string.IsNullOrEmpty(episodesJson)) + return null; + + return JsonSerializer.Deserialize(episodesJson); + } + + async ValueTask> 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 res)) + return res; + + try + { + var headers = new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) }; + + async Task> 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(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 { firstResult }; + hybridCache.Set(memKey, list, cacheTime(5)); + return list; + } + + return null; + } + catch (Exception ex) + { + OnLog($"AnimeON error: {ex.Message}"); + } + + return null; + } + } +} diff --git a/AnimeON/ModInit.cs b/AnimeON/ModInit.cs new file mode 100644 index 0000000..c8e9d3a --- /dev/null +++ b/AnimeON/ModInit.cs @@ -0,0 +1,24 @@ +using Shared; +using Shared.Models.Online.Settings; + +namespace AnimeON +{ + public class ModInit + { + public static OnlinesSettings AnimeON; + + /// + /// модуль загружен + /// + public static void loaded() + { + AnimeON = new OnlinesSettings("AnimeON", "https://animeon.club", streamproxy: false) + { + displayname = "🇯🇵 AnimeON" + }; + + // Виводити "уточнити пошук" + AppInit.conf.online.with_search.Add("animeon"); + } + } +} \ No newline at end of file diff --git a/AnimeON/Models/Models.cs b/AnimeON/Models/Models.cs new file mode 100644 index 0000000..c45874e --- /dev/null +++ b/AnimeON/Models/Models.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AnimeON.Models +{ + public class SearchResponseModel + { + [JsonPropertyName("result")] + public List Result { get; set; } + + [JsonPropertyName("count")] + public int Count { get; set; } + } + + public class SearchModel + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("titleUa")] + public string TitleUa { get; set; } + + [JsonPropertyName("titleEn")] + public string TitleEn { get; set; } + + [JsonPropertyName("releaseDate")] + public string Year { get; set; } + + [JsonPropertyName("imdbId")] + public string ImdbId { get; set; } + + [JsonPropertyName("season")] + public int Season { get; set; } + } + + public class FundubsResponseModel + { + [JsonPropertyName("funDubs")] + public List FunDubs { get; set; } + } + + public class FundubModel + { + [JsonPropertyName("fundub")] + public Fundub Fundub { get; set; } + + [JsonPropertyName("player")] + public List Player { get; set; } + } + + public class Fundub + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + } + + public class Player + { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("id")] + public int Id { get; set; } + } + + public class EpisodeModel + { + [JsonPropertyName("episodes")] + public List Episodes { get; set; } + } + + public class Episode + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("episode")] + public int EpisodeNum { get; set; } + + [JsonPropertyName("fileUrl")] + public string Hls { get; set; } + + [JsonPropertyName("videoUrl")] + public string VideoUrl { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + } + + public class Movie + { + public string translation { get; set; } + public List<(string link, string quality)> links { get; set; } + public Shared.Models.Templates.SubtitleTpl? subtitles { get; set; } + public int season { get; set; } + public int episode { get; set; } + } + + public class Result + { + public List movie { get; set; } + } +} \ No newline at end of file diff --git a/AnimeON/OnlineApi.cs b/AnimeON/OnlineApi.cs new file mode 100644 index 0000000..2d9d0cc --- /dev/null +++ b/AnimeON/OnlineApi.cs @@ -0,0 +1,25 @@ +using Shared.Models.Base; +using System.Collections.Generic; + +namespace AnimeON +{ + public class OnlineApi + { + 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.AnimeON; + if (init.enable && !init.rip) + { + string url = init.overridehost; + if (string.IsNullOrEmpty(url)) + url = $"{host}/animeon"; + + online.Add((init.displayname, url, "animeon", init.displayindex > 0 ? init.displayindex : online.Count)); + } + + return online; + } + } +} diff --git a/AnimeON/manifest.json b/AnimeON/manifest.json new file mode 100644 index 0000000..0c653cb --- /dev/null +++ b/AnimeON/manifest.json @@ -0,0 +1,6 @@ +{ + "enable": true, + "version": 1, + "initspace": "AnimeON.ModInit", + "online": "AnimeON.OnlineApi" +} \ No newline at end of file