diff --git a/Anihub/Anihub.csproj b/Anihub/Anihub.csproj
new file mode 100644
index 0000000..11a5084
--- /dev/null
+++ b/Anihub/Anihub.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net9.0
+ library
+ true
+
+
+
+
+ ..\..\Shared.dll
+
+
+
+
diff --git a/Anihub/AnihubInvoke.cs b/Anihub/AnihubInvoke.cs
new file mode 100644
index 0000000..91548ec
--- /dev/null
+++ b/Anihub/AnihubInvoke.cs
@@ -0,0 +1,244 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Shared;
+using Shared.Models.Online.Settings;
+using Shared.Models;
+using System.Text.Json;
+using System.Linq;
+using Anihub.Models;
+using Shared.Engine;
+using System.Net;
+using System.Web;
+using Microsoft.Extensions.Caching.Memory;
+using Shared.Models.Templates;
+using System.Net.Http;
+using System.Text;
+
+namespace Anihub
+{
+ public class AnihubInvoke
+ {
+ private OnlinesSettings _init;
+ private HybridCache _hybridCache;
+ private Action _onLog;
+ private ProxyManager _proxyManager;
+
+ public AnihubInvoke(OnlinesSettings init, HybridCache hybridCache, Action onLog, ProxyManager proxyManager)
+ {
+ _init = init;
+ _hybridCache = hybridCache;
+ _onLog = onLog;
+ _proxyManager = proxyManager;
+ }
+
+ public async ValueTask Search(string title, string original_title, string year, string t)
+ {
+ var headers = HeadersModel.Init(
+ ("Referer", _init.host)
+ );
+
+ string searchQuery = string.IsNullOrEmpty(title) ? original_title : title;
+ string searchUrl = $"{_init.apihost}/anime/?search={HttpUtility.UrlEncode(searchQuery)}";
+
+ string response = await Http.Get(searchUrl, headers: headers, proxy: _proxyManager.Get());
+
+ if (string.IsNullOrEmpty(response))
+ {
+ _onLog?.Invoke($"Anihub search failed for: {searchQuery}");
+ return null;
+ }
+
+ try
+ {
+ var searchResponse = JsonSerializer.Deserialize(response);
+ _onLog?.Invoke($"Anihub search: {searchQuery} -> {searchResponse?.Count ?? 0} results");
+ return searchResponse;
+ }
+ catch (Exception ex)
+ {
+ _onLog?.Invoke($"Anihub search parse error: {searchQuery} - {ex.Message}");
+ return null;
+ }
+ }
+
+ public async ValueTask Embed(string animeId, string? searchUri = null)
+ {
+ if (!int.TryParse(animeId, out int parsedAnimeId))
+ {
+ _onLog?.Invoke($"Anihub embed: invalid animeId {animeId}");
+ return null;
+ }
+
+ var headers = HeadersModel.Init(
+ ("Referer", _init.host)
+ );
+
+ string sourcesUrl = $"{_init.apihost}/episode-sources/{parsedAnimeId}";
+ string response = await Http.Get(sourcesUrl, headers: headers, proxy: _proxyManager.Get());
+
+ if (string.IsNullOrEmpty(response))
+ {
+ _onLog?.Invoke($"Anihub sources failed for animeId: {parsedAnimeId}");
+ return null;
+ }
+
+ try
+ {
+ var sourcesResponse = JsonSerializer.Deserialize(response);
+ int moonCount = sourcesResponse?.Moonanime?.Count ?? 0;
+ int ashdiCount = sourcesResponse?.Ashdi?.Count ?? 0;
+ _onLog?.Invoke($"Anihub sources: animeId {parsedAnimeId} -> {moonCount} Moon, {ashdiCount} Ashdi");
+ return sourcesResponse;
+ }
+ catch (Exception ex)
+ {
+ _onLog?.Invoke($"Anihub sources parse error: animeId {parsedAnimeId} - {ex.Message}");
+ return null;
+ }
+ }
+
+ public async Task Auth()
+ {
+ // Видаляємо цей метод, оскільки OnlinesSettings не має login/passwd полів
+ // або можна додати перевірку на token з _init
+ return null;
+ }
+
+ public async Task> GetMovies(AnihubResult anime)
+ {
+ var movies = new List();
+
+ try
+ {
+ var movie = new MovieTpl(anime.TitleUkrainian, anime.TitleEnglish, 1);
+ movies.Add(movie);
+ }
+ catch (Exception ex)
+ {
+ _onLog?.Invoke($"Anihub GetMovies error: {ex.Message}");
+ }
+
+ return movies;
+ }
+
+ public async Task> GetVoices(AnihubEpisodeSourcesResponse sources)
+ {
+ var voices = new List();
+
+ try
+ {
+ var voice_tpl = new VoiceTpl();
+
+ // Add Moonanime sources
+ foreach (var source in sources.Moonanime)
+ {
+ string voiceName = $"[Moon] {source.StudioName}";
+ string voiceToken = $"moonanime:{source.Id}";
+ voice_tpl.Append(voiceName, false, voiceToken);
+ }
+
+ // Add Ashdi sources
+ foreach (var source in sources.Ashdi)
+ {
+ string voiceName = $"[Ashdi] {source.StudioName}";
+ string voiceToken = $"ashdi:{source.Id}";
+ voice_tpl.Append(voiceName, false, voiceToken);
+ }
+
+ voices.Add(voice_tpl);
+ }
+ catch (Exception ex)
+ {
+ _onLog?.Invoke($"Anihub GetVoices error: {ex.Message}");
+ }
+
+ return voices;
+ }
+
+ public async Task> GetSeasons(AnihubEpisodeSourcesResponse sources)
+ {
+ var seasons = new List();
+
+ try
+ {
+ var seasonNumbers = new HashSet();
+
+ // Collect unique season numbers from Moonanime sources
+ foreach (var source in sources.Moonanime)
+ {
+ seasonNumbers.Add(source.SeasonNumber);
+ }
+
+ // Collect unique season numbers from Ashdi sources
+ foreach (var source in sources.Ashdi)
+ {
+ seasonNumbers.Add(source.SeasonNumber);
+ }
+
+ // Create season templates
+ var season_tpl = new SeasonTpl();
+ foreach (var seasonNum in seasonNumbers.OrderBy(x => x))
+ {
+ string seasonName = $"Сезон {seasonNum}";
+ string seasonToken = seasonNum.ToString();
+ season_tpl.Append(seasonName, seasonToken, seasonToken);
+ }
+
+ seasons.Add(season_tpl);
+ }
+ catch (Exception ex)
+ {
+ _onLog?.Invoke($"Anihub GetSeasons error: {ex.Message}");
+ }
+
+ return seasons;
+ }
+
+ public async Task> GetEpisodes(AnihubEpisodeSourcesResponse sources, int seasonId)
+ {
+ var episodes = new List();
+
+ try
+ {
+ var episode_tpl = new EpisodeTpl();
+
+ // Get episodes from Moonanime sources
+ var moonanimeSource = sources.Moonanime.FirstOrDefault(s => s.SeasonNumber == seasonId);
+ if (moonanimeSource != null)
+ {
+ foreach (var ep in moonanimeSource.Episodes.OrderBy(e => e.EpisodeNumber))
+ {
+ string episodeName = ep.Title ?? $"Епізод {ep.EpisodeNumber}";
+ string seasonStr = seasonId.ToString();
+ string episodeStr = ep.EpisodeNumber.ToString();
+ string link = $"{_init.host}/embed/{ep.Id}";
+ episode_tpl.Append(episodeName, "Anihub", seasonStr, episodeStr, link, "call");
+ }
+ }
+
+ // Get episodes from Ashdi sources
+ var ashdiSource = sources.Ashdi.FirstOrDefault(s => s.SeasonNumber == seasonId);
+ if (ashdiSource != null)
+ {
+ foreach (var ep in ashdiSource.EpisodesData.OrderBy(e => e.EpisodeNumber))
+ {
+ string episodeName = ep.Title ?? $"Епізод {ep.EpisodeNumber} (Ashdi)";
+ string seasonStr = seasonId.ToString();
+ string episodeStr = ep.EpisodeNumber.ToString();
+ string link = ep.Url ?? $"{_init.host}/embed/{ep.Id}";
+ episode_tpl.Append(episodeName, "Anihub", seasonStr, episodeStr, link, "call");
+ }
+ }
+
+ episodes.Add(episode_tpl);
+ }
+ catch (Exception ex)
+ {
+ _onLog?.Invoke($"Anihub GetEpisodes error: {ex.Message}");
+ }
+
+ return episodes;
+ }
+ }
+}
diff --git a/Anihub/Controller.cs b/Anihub/Controller.cs
new file mode 100644
index 0000000..bb3eef2
--- /dev/null
+++ b/Anihub/Controller.cs
@@ -0,0 +1,353 @@
+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 Anihub.Models;
+using System.Text.RegularExpressions;
+using Shared.Models.Online.Settings;
+using Shared.Models;
+using System.Net;
+using System.Net.Http;
+
+namespace Anihub
+{
+ [Route("anihub")]
+ public class AnihubController : BaseOnlineController
+ {
+ ProxyManager proxyManager;
+
+ public AnihubController()
+ {
+ proxyManager = new ProxyManager(ModInit.Anihub);
+ }
+
+ [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, bool rjson = false)
+ {
+ var init = await loadKit(ModInit.Anihub);
+ if (!init.enable)
+ return OnError();
+
+ OnLog($"Anihub: {title} (serial={serial}, s={s}, t={t})");
+
+ var invoke = new AnihubInvoke(init, hybridCache, OnLog, proxyManager);
+
+ var searchResponse = await invoke.Search(title, original_title, "0", year.ToString());
+ if (searchResponse == null || searchResponse.IsEmpty)
+ return OnError();
+
+ if (serial == 1)
+ {
+ if (s == -1) // Відображення списку аніме як "сезонів"
+ {
+ var season_tpl = new SeasonTpl();
+ for (int i = 0; i < searchResponse.content.Count; i++)
+ {
+ var anime = searchResponse.content[i];
+ string seasonName = anime.TitleUkrainian ?? anime.TitleEnglish ?? anime.TitleOriginal;
+ string link = $"{host}/anihub?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, i.ToString());
+ }
+
+ OnLog($"Anihub: generated {searchResponse.content.Count} seasons");
+ return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8");
+ }
+ else // Відображення озвучок та епізодів для вибраного аніме
+ {
+ if (s >= searchResponse.content.Count)
+ return OnError();
+
+ var selectedAnime = searchResponse.content[s];
+ var episodesResponse = await invoke.Embed(selectedAnime.Id.ToString());
+ if (episodesResponse == null)
+ return OnError();
+
+ var voice_tpl = new VoiceTpl();
+ var episode_tpl = new EpisodeTpl();
+
+ // Автоматично вибираємо першу озвучку якщо не вибрана
+ string selectedVoice = t;
+ if (string.IsNullOrEmpty(selectedVoice))
+ {
+ if (episodesResponse.Moonanime.Count > 0)
+ selectedVoice = $"moon_{episodesResponse.Moonanime.First().Id}";
+ else if (episodesResponse.Ashdi.Count > 0)
+ selectedVoice = $"ashdi_{episodesResponse.Ashdi.First().Id}";
+ }
+
+ // Додаємо озвучки з Moonanime
+ foreach (var moonSource in episodesResponse.Moonanime)
+ {
+ string voiceName = $"[Moon] {moonSource.StudioName}";
+ string voiceLink = $"{host}/anihub?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=moon_{moonSource.Id}";
+ bool isActive = selectedVoice != null && selectedVoice.StartsWith("moon_") && selectedVoice.Split('_')[1] == moonSource.Id.ToString();
+ voice_tpl.Append(voiceName, isActive, voiceLink);
+ }
+
+ // Додаємо озвучки з Ashdi
+ foreach (var ashdiSource in episodesResponse.Ashdi)
+ {
+ string voiceName = $"[Ashdi] {ashdiSource.StudioName}";
+ string voiceLink = $"{host}/anihub?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=ashdi_{ashdiSource.Id}";
+ bool isActive = selectedVoice != null && selectedVoice.StartsWith("ashdi_") && selectedVoice.Split('_')[1] == ashdiSource.Id.ToString();
+ voice_tpl.Append(voiceName, isActive, voiceLink);
+ }
+
+ // Завжди додаємо епізоди для вибраної озвучки (або автоматично вибраної)
+ if (!string.IsNullOrEmpty(selectedVoice))
+ {
+ if (selectedVoice.StartsWith("moon_"))
+ {
+ var parts = selectedVoice.Split('_');
+ if (parts.Length >= 2 && int.TryParse(parts[1], out int sourceId))
+ {
+ var selectedSource = episodesResponse.Moonanime.FirstOrDefault(m => m.Id == sourceId);
+ if (selectedSource != null)
+ {
+ foreach (var episode in selectedSource.Episodes.OrderBy(e => e.EpisodeNumber))
+ {
+ string episodeName = !string.IsNullOrEmpty(episode.Title) ? episode.Title : $"Епізод {episode.EpisodeNumber}";
+ string episodeLink = $"{host}/anihub/play?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&s={s}&e={episode.EpisodeNumber}&t={selectedVoice}_{episode.Id}";
+ episode_tpl.Append(episodeName, title ?? original_title, s.ToString(), episode.EpisodeNumber.ToString("D2"), episodeLink, "call");
+ }
+ }
+ }
+ }
+ else if (selectedVoice.StartsWith("ashdi_"))
+ {
+ var parts = selectedVoice.Split('_');
+ if (parts.Length >= 2 && int.TryParse(parts[1], out int sourceId))
+ {
+ var selectedSource = episodesResponse.Ashdi.FirstOrDefault(a => a.Id == sourceId);
+ if (selectedSource != null)
+ {
+ foreach (var episodeData in selectedSource.EpisodesData.OrderBy(e => e.EpisodeNumber))
+ {
+ string episodeName = !string.IsNullOrEmpty(episodeData.Title) ? episodeData.Title : $"Епізод {episodeData.EpisodeNumber}";
+ string episodeLink = $"{host}/anihub/play?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&s={s}&e={episodeData.EpisodeNumber}&t={selectedVoice}_{episodeData.Id}";
+ episode_tpl.Append(episodeName, title ?? original_title, s.ToString(), episodeData.EpisodeNumber.ToString("D2"), episodeLink, "call");
+ }
+ }
+ }
+ }
+ }
+
+ int voiceCount = episodesResponse.Moonanime.Count + episodesResponse.Ashdi.Count;
+ int episodeCount = 0;
+ if (!string.IsNullOrEmpty(selectedVoice))
+ {
+ if (selectedVoice.StartsWith("moon_"))
+ {
+ var parts = selectedVoice.Split('_');
+ if (parts.Length >= 2 && int.TryParse(parts[1], out int sourceId))
+ {
+ var selectedSource = episodesResponse.Moonanime.FirstOrDefault(m => m.Id == sourceId);
+ episodeCount = selectedSource?.Episodes?.Count ?? 0;
+ }
+ }
+ else if (selectedVoice.StartsWith("ashdi_"))
+ {
+ var parts = selectedVoice.Split('_');
+ if (parts.Length >= 2 && int.TryParse(parts[1], out int sourceId))
+ {
+ var selectedSource = episodesResponse.Ashdi.FirstOrDefault(a => a.Id == sourceId);
+ episodeCount = selectedSource?.EpisodesData?.Count ?? 0;
+ }
+ }
+ }
+
+ OnLog($"Anihub: generated {voiceCount} voices, {episodeCount} episodes");
+
+ 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 = searchResponse.content.FirstOrDefault();
+ if (firstAnime == null)
+ return OnError();
+
+ var episodesResponse = await invoke.Embed(firstAnime.Id.ToString());
+ if (episodesResponse == null)
+ return OnError();
+
+ var movie_tpl = new MovieTpl(title, original_title);
+
+ // Обробляємо джерела Moonanime
+ foreach (var moonSource in episodesResponse.Moonanime)
+ {
+ foreach (var episode in moonSource.Episodes)
+ {
+ string voiceName = $"[Moon] {moonSource.StudioName}";
+ string link = $"{host}/anihub/play?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&s=0&e={episode.EpisodeNumber}&t=moon_{moonSource.Id}_{episode.Id}";
+ movie_tpl.Append(voiceName, link, "call");
+ }
+ }
+
+ // Обробляємо джерела Ashdi
+ foreach (var ashdiSource in episodesResponse.Ashdi)
+ {
+ foreach (var episodeData in ashdiSource.EpisodesData)
+ {
+ string voiceName = $"[Ashdi] {ashdiSource.StudioName}";
+ string link = $"{host}/anihub/play?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&s=0&e={episodeData.EpisodeNumber}&t=ashdi_{ashdiSource.Id}_{episodeData.Id}";
+ movie_tpl.Append(voiceName, link, "call");
+ }
+ }
+
+ int totalOptions = 0;
+ foreach (var moonSource in episodesResponse.Moonanime)
+ totalOptions += moonSource.Episodes.Count;
+ foreach (var ashdiSource in episodesResponse.Ashdi)
+ totalOptions += ashdiSource.EpisodesData.Count;
+
+ OnLog($"Anihub: generated {totalOptions} movie options");
+ return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
+ }
+ }
+
+ [HttpGet]
+ [Route("play")]
+ async public Task Play(string imdb_id, long kinopoisk_id, string title, string original_title, int year, int s, int e, string t, bool play = false)
+ {
+ var init = await loadKit(ModInit.Anihub);
+ if (!init.enable)
+ return OnError();
+
+ OnLog($"Anihub play: {title} s={s} e={e} ({t})");
+
+ var invoke = new AnihubInvoke(init, hybridCache, OnLog, proxyManager);
+
+ // Парсимо токен озвучки/джерела
+ if (string.IsNullOrEmpty(t))
+ return OnError();
+
+ var parts = t.Split('_');
+ if (parts.Length < 3)
+ return OnError();
+
+ string sourceType = parts[0];
+ if (!int.TryParse(parts[1], out int sourceId) || !int.TryParse(parts[2], out int episodeId))
+ return OnError();
+
+ // Знаходимо аніме та отримуємо джерела епізодів
+ var searchResponse = await invoke.Search(title, original_title, "0", year.ToString());
+ if (searchResponse == null || searchResponse.IsEmpty || s >= searchResponse.content.Count)
+ return OnError();
+
+ var selectedAnime = searchResponse.content[s];
+ var episodesResponse = await invoke.Embed(selectedAnime.Id.ToString());
+ if (episodesResponse == null)
+ return OnError();
+
+ string iframeUrl = null;
+
+ if (sourceType == "moon")
+ {
+ var moonSource = episodesResponse.Moonanime.FirstOrDefault(m => m.Id == sourceId);
+ var episode = moonSource?.Episodes.FirstOrDefault(ep => ep.Id == episodeId);
+ iframeUrl = episode?.IframeLink;
+ }
+ else if (sourceType == "ashdi")
+ {
+ var ashdiSource = episodesResponse.Ashdi.FirstOrDefault(a => a.Id == sourceId);
+ var episodeData = ashdiSource?.EpisodesData.FirstOrDefault(ep => ep.Id == episodeId.ToString());
+ iframeUrl = episodeData?.VodUrl;
+ }
+
+ if (string.IsNullOrEmpty(iframeUrl))
+ {
+ OnLog($"Anihub play: iframe URL not found for {sourceType}");
+ return OnError();
+ }
+
+ // Отримуємо пряме посилання на потік
+ string streamUrl = await ExtractStreamUrl(iframeUrl, sourceType);
+
+ if (string.IsNullOrEmpty(streamUrl))
+ {
+ OnLog($"Anihub play: stream extraction failed for {sourceType}");
+ return OnError();
+ }
+
+ OnLog($"Anihub play: extracted {sourceType} stream");
+
+ // Якщо play=true, робимо Redirect, інакше повертаємо JSON
+ if (play)
+ return Redirect(streamUrl);
+ else
+ return Content(VideoTpl.ToJson("play", streamUrl, title ?? original_title), "application/json; charset=utf-8");
+ }
+
+ private async Task ExtractStreamUrl(string iframeUrl, string sourceType)
+ {
+ try
+ {
+ string requestUrl = iframeUrl;
+
+ // Додаємо параметр player тільки для Moon
+ if (sourceType == "moon")
+ {
+ requestUrl = iframeUrl + (iframeUrl.Contains("?") ? "&" : "?") + $"player={host}";
+ }
+
+ // Створюємо HTTP клієнт з правильними заголовками
+ using var httpClient = new HttpClient();
+
+ if (sourceType == "moon")
+ {
+ httpClient.DefaultRequestHeaders.Add("Referer", "https://moonanime.art/");
+ httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
+ httpClient.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8");
+ httpClient.DefaultRequestHeaders.Add("Accept-Language", "uk-UA,uk;q=0.9,en-US;q=0.8,en;q=0.7");
+ }
+ else
+ {
+ httpClient.DefaultRequestHeaders.Add("Referer", host);
+ httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0.1 Safari/605.1.15");
+ }
+
+ // Робимо запит до плеєра
+ OnLog($"Anihub player: requesting {sourceType} iframe");
+ var response = await httpClient.GetStringAsync(requestUrl);
+
+ if (string.IsNullOrEmpty(response))
+ return null;
+
+ // Парсимо відповідь для отримання file URL
+ string streamUrl = null;
+
+ if (sourceType == "ashdi")
+ {
+ var match = Regex.Match(response, @"file:'([^']+)'");
+ if (match.Success)
+ streamUrl = match.Groups[1].Value;
+ }
+ else if (sourceType == "moon")
+ {
+ var match = Regex.Match(response, @"file:\s*""([^""]+)""");
+ if (match.Success)
+ streamUrl = match.Groups[1].Value;
+ }
+
+ if (!string.IsNullOrEmpty(streamUrl))
+ OnLog($"Anihub player: extracted {sourceType} stream");
+
+ return streamUrl;
+ }
+ catch (Exception ex)
+ {
+ OnLog($"Anihub player: {sourceType} error - {ex.Message}");
+ return null;
+ }
+ }
+ }
+}
diff --git a/Anihub/ModInit.cs b/Anihub/ModInit.cs
new file mode 100644
index 0000000..4bf98de
--- /dev/null
+++ b/Anihub/ModInit.cs
@@ -0,0 +1,38 @@
+using Newtonsoft.Json;
+using Shared;
+using Shared.Engine;
+using Newtonsoft.Json.Linq;
+using Shared.Models.Online.Settings;
+using Shared.Models.Module;
+
+namespace Anihub
+{
+ public class ModInit
+ {
+ public static OnlinesSettings Anihub;
+
+ ///
+ /// модуль загружен
+ ///
+ public static void loaded(InitspaceModel initspace)
+ {
+ Anihub = new OnlinesSettings("Anihub", "https://anihub.in.ua", streamproxy: false, useproxy: false)
+ {
+ displayname = "🇺🇦 Anihub",
+ displayindex = 0,
+ apihost = "https://anihub.in.ua/api",
+ proxy = new Shared.Models.Base.ProxySettings()
+ {
+ useAuth = true,
+ username = "a",
+ password = "a",
+ list = new string[] { "socks5://IP:PORT" }
+ }
+ };
+ Anihub = ModuleInvoke.Conf("Anihub", Anihub).ToObject();
+
+ // Виводити "уточнити пошук"
+ AppInit.conf.online.with_search.Add("anihub");
+ }
+ }
+}
diff --git a/Anihub/Models/AnihubModels.cs b/Anihub/Models/AnihubModels.cs
new file mode 100644
index 0000000..cac25ea
--- /dev/null
+++ b/Anihub/Models/AnihubModels.cs
@@ -0,0 +1,331 @@
+using System;
+using System.Text.Json.Serialization;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Anihub.Models
+{
+ public class AnihubSearchResponse
+ {
+ [JsonPropertyName("count")]
+ public int Count { get; set; }
+
+ [JsonPropertyName("next")]
+ public object? Next { get; set; }
+
+ [JsonPropertyName("previous")]
+ public object? Previous { get; set; }
+
+ [JsonPropertyName("results")]
+ public List Results { get; set; } = new();
+
+ public bool IsEmpty => Results == null || Results.Count == 0;
+ public List content => Results ?? new List();
+ }
+
+ public class AnihubResult
+ {
+ [JsonPropertyName("id")]
+ public int Id { get; set; }
+
+ [JsonPropertyName("slug")]
+ public string Slug { get; set; } = string.Empty;
+
+ [JsonPropertyName("title_ukrainian")]
+ public string TitleUkrainian { get; set; } = string.Empty;
+
+ [JsonPropertyName("title_english")]
+ public string TitleEnglish { get; set; } = string.Empty;
+
+ [JsonPropertyName("title_original")]
+ public string TitleOriginal { get; set; } = string.Empty;
+
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = string.Empty;
+
+ [JsonPropertyName("poster_url")]
+ public string PosterUrl { get; set; } = string.Empty;
+
+ [JsonPropertyName("banner_url")]
+ public string BannerUrl { get; set; } = string.Empty;
+
+ [JsonPropertyName("status")]
+ public string Status { get; set; } = string.Empty;
+
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = string.Empty;
+
+ [JsonPropertyName("year")]
+ public int Year { get; set; }
+
+ [JsonPropertyName("season_number")]
+ public int SeasonNumber { get; set; }
+
+ [JsonPropertyName("episodes_count")]
+ public int EpisodesCount { get; set; }
+
+ [JsonPropertyName("rating")]
+ public double Rating { get; set; }
+
+ [JsonPropertyName("genres")]
+ public List Genres { get; set; } = new();
+
+ [JsonPropertyName("is_recommended_by_community")]
+ public bool IsRecommendedByCommunity { get; set; }
+
+ [JsonPropertyName("is_nsfw")]
+ public bool IsNsfw { get; set; }
+
+ [JsonPropertyName("has_ukrainian_dub")]
+ public bool HasUkrainianDub { get; set; }
+ }
+
+ public class AnihubGenre
+ {
+ [JsonPropertyName("id")]
+ public int Id { get; set; }
+
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonPropertyName("name_ukrainian")]
+ public string NameUkrainian { get; set; } = string.Empty;
+ }
+
+ public class AnihubEpisodeSourcesResponse
+ {
+ [JsonPropertyName("moonanime")]
+ public List Moonanime { get; set; } = new();
+
+ [JsonPropertyName("ashdi")]
+ public List Ashdi { get; set; } = new();
+
+ [JsonPropertyName("statistics")]
+ public AnihubStatistics Statistics { get; set; } = new();
+
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = string.Empty;
+ }
+
+ public class MoonanimeSource
+ {
+ [JsonPropertyName("id")]
+ public int Id { get; set; }
+
+ [JsonPropertyName("studio_name")]
+ public string StudioName { get; set; } = string.Empty;
+
+ [JsonPropertyName("season_number")]
+ public int SeasonNumber { get; set; }
+
+ [JsonPropertyName("episodes")]
+ public List Episodes { get; set; } = new();
+
+ [JsonPropertyName("episodes_count")]
+ public int EpisodesCount { get; set; }
+
+ [JsonPropertyName("created_at")]
+ public string CreatedAt { get; set; } = string.Empty;
+
+ [JsonPropertyName("updated_at")]
+ public string UpdatedAt { get; set; } = string.Empty;
+ }
+
+ public class MoonanimeEpisode
+ {
+ [JsonPropertyName("id")]
+ public int Id { get; set; }
+
+ [JsonPropertyName("episode_number")]
+ public int EpisodeNumber { get; set; }
+
+ [JsonPropertyName("title")]
+ public string Title { get; set; } = string.Empty;
+
+ [JsonPropertyName("title_en")]
+ public string TitleEn { get; set; } = string.Empty;
+
+ [JsonPropertyName("title_jp")]
+ public string TitleJp { get; set; } = string.Empty;
+
+ [JsonPropertyName("iframe_link")]
+ public string IframeLink { get; set; } = string.Empty;
+
+ [JsonPropertyName("vod_link")]
+ public string VodLink { get; set; } = string.Empty;
+
+ [JsonPropertyName("poster_url")]
+ public string PosterUrl { get; set; } = string.Empty;
+
+ [JsonPropertyName("episode_type")]
+ public string EpisodeType { get; set; } = string.Empty;
+
+ [JsonPropertyName("release_date")]
+ public string ReleaseDate { get; set; } = string.Empty;
+
+ [JsonPropertyName("embed_url")]
+ public EmbedUrl EmbedUrl { get; set; } = new();
+
+ [JsonPropertyName("created_at")]
+ public string CreatedAt { get; set; } = string.Empty;
+
+ [JsonPropertyName("updated_at")]
+ public string UpdatedAt { get; set; } = string.Empty;
+ }
+
+ public class EmbedUrl
+ {
+ [JsonPropertyName("iframe_url")]
+ public string IframeUrl { get; set; } = string.Empty;
+
+ [JsonPropertyName("iframe_code")]
+ public string IframeCode { get; set; } = string.Empty;
+
+ [JsonPropertyName("width")]
+ public int Width { get; set; }
+
+ [JsonPropertyName("height")]
+ public int Height { get; set; }
+ }
+
+ public class AshdiSource
+ {
+ [JsonPropertyName("id")]
+ public int Id { get; set; }
+
+ [JsonPropertyName("studio_name")]
+ public string StudioName { get; set; } = string.Empty;
+
+ [JsonPropertyName("season_number")]
+ public int SeasonNumber { get; set; }
+
+ [JsonPropertyName("episodes_data")]
+ public List EpisodesData { get; set; } = new();
+
+ [JsonPropertyName("episode_urls")]
+ public List EpisodeUrls { get; set; } = new();
+
+ [JsonPropertyName("episodes_count")]
+ public int EpisodesCount { get; set; }
+
+ [JsonPropertyName("created_at")]
+ public string CreatedAt { get; set; } = string.Empty;
+
+ [JsonPropertyName("updated_at")]
+ public string UpdatedAt { get; set; } = string.Empty;
+ }
+
+ public class AshdiEpisodeData
+ {
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ [JsonPropertyName("url")]
+ public string Url { get; set; } = string.Empty;
+
+ [JsonPropertyName("title")]
+ public string Title { get; set; } = string.Empty;
+
+ [JsonPropertyName("vod_url")]
+ public string VodUrl { get; set; } = string.Empty;
+
+ [JsonPropertyName("episode_number")]
+ public int EpisodeNumber { get; set; }
+ }
+
+ public class AshdiEpisodeUrl
+ {
+ [JsonPropertyName("episode_number")]
+ public int EpisodeNumber { get; set; }
+
+ [JsonPropertyName("title")]
+ public string Title { get; set; } = string.Empty;
+
+ [JsonPropertyName("url")]
+ public string Url { get; set; } = string.Empty;
+
+ [JsonPropertyName("ashdi_episode_id")]
+ public string AshdiEpisodeId { get; set; } = string.Empty;
+ }
+
+ public class AnihubStatistics
+ {
+ [JsonPropertyName("anime_title")]
+ public string AnimeTitle { get; set; } = string.Empty;
+
+ [JsonPropertyName("season_number")]
+ public int SeasonNumber { get; set; }
+
+ [JsonPropertyName("moonanime")]
+ public MoonanimeStats Moonanime { get; set; } = new();
+
+ [JsonPropertyName("ashdi")]
+ public AshdiStats Ashdi { get; set; } = new();
+
+ [JsonPropertyName("total_unique_studios")]
+ public int TotalUniqueStudios { get; set; }
+ }
+
+ public class MoonanimeStats
+ {
+ [JsonPropertyName("total_records")]
+ public int TotalRecords { get; set; }
+
+ [JsonPropertyName("unique_count")]
+ public int UniqueCount { get; set; }
+
+ [JsonPropertyName("unique_names")]
+ public List UniqueNames { get; set; } = new();
+
+ [JsonPropertyName("details")]
+ public List Details { get; set; } = new();
+ }
+
+ public class MoonanimeDetail
+ {
+ [JsonPropertyName("original_name")]
+ public string OriginalName { get; set; } = string.Empty;
+
+ [JsonPropertyName("normalized_name")]
+ public string NormalizedName { get; set; } = string.Empty;
+
+ [JsonPropertyName("season")]
+ public int Season { get; set; }
+
+ [JsonPropertyName("moon_id")]
+ public int MoonId { get; set; }
+
+ [JsonPropertyName("episodes_count")]
+ public int EpisodesCount { get; set; }
+ }
+
+ public class AshdiStats
+ {
+ [JsonPropertyName("total_records")]
+ public int TotalRecords { get; set; }
+
+ [JsonPropertyName("unique_count")]
+ public int UniqueCount { get; set; }
+
+ [JsonPropertyName("unique_names")]
+ public List UniqueNames { get; set; } = new();
+
+ [JsonPropertyName("details")]
+ public List Details { get; set; } = new();
+ }
+
+ public class AshdiDetail
+ {
+ [JsonPropertyName("original_name")]
+ public string OriginalName { get; set; } = string.Empty;
+
+ [JsonPropertyName("normalized_name")]
+ public string NormalizedName { get; set; } = string.Empty;
+
+ [JsonPropertyName("season")]
+ public int Season { get; set; }
+
+ [JsonPropertyName("episodes_count")]
+ public int EpisodesCount { get; set; }
+ }
+}
diff --git a/Anihub/OnlineApi.cs b/Anihub/OnlineApi.cs
new file mode 100644
index 0000000..5d78bf6
--- /dev/null
+++ b/Anihub/OnlineApi.cs
@@ -0,0 +1,25 @@
+using Shared.Models.Base;
+using System.Collections.Generic;
+
+namespace Anihub
+{
+ 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.Anihub;
+ if (init.enable && !init.rip)
+ {
+ string url = init.overridehost;
+ if (string.IsNullOrEmpty(url))
+ url = $"{host}/anihub";
+
+ online.Add((init.displayname, url, "anihub", init.displayindex));
+ }
+
+ return online;
+ }
+ }
+}
diff --git a/Anihub/manifest.json b/Anihub/manifest.json
new file mode 100644
index 0000000..3cc2b60
--- /dev/null
+++ b/Anihub/manifest.json
@@ -0,0 +1,6 @@
+{
+ "enable": true,
+ "version": 2,
+ "initspace": "Anihub.ModInit",
+ "online": "Anihub.OnlineApi"
+}