diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8f4dd64..0000000 --- a/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -/.idea/ -/AIDocumentation/ -/LampaC/ -/BanderaBackend/ -/Kinovezha/ -/.clinerules/moduls.md -/.clinerules/uaflix-optimization.md -/.clinerules/ -/.qodo/ \ No newline at end of file diff --git a/AnimeON/AnimeON.csproj b/AnimeON/AnimeON.csproj deleted file mode 100644 index c26a806..0000000 --- a/AnimeON/AnimeON.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net9.0 - library - true - - - - - ..\..\Shared.dll - - - - \ No newline at end of file diff --git a/AnimeON/AnimeONInvoke.cs b/AnimeON/AnimeONInvoke.cs deleted file mode 100644 index 42c0789..0000000 --- a/AnimeON/AnimeONInvoke.cs +++ /dev/null @@ -1,352 +0,0 @@ -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 System.Text; -using AnimeON.Models; -using Shared.Engine; - -namespace AnimeON -{ - public class AnimeONInvoke - { - private static readonly HashSet NotAllowedHosts = - new HashSet( - new[] - { - "c3ZpdGFubW92aWU=", - "cG9ydGFsLXR2", - } - .Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))), - StringComparer.OrdinalIgnoreCase - ); - private OnlinesSettings _init; - private HybridCache _hybridCache; - private Action _onLog; - private ProxyManager _proxyManager; - - public AnimeONInvoke(OnlinesSettings init, HybridCache hybridCache, Action onLog, ProxyManager proxyManager) - { - _init = init; - _hybridCache = hybridCache; - _onLog = onLog; - _proxyManager = proxyManager; - } - - public async Task> Search(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={System.Web.HttpUtility.UrlEncode(query)}"; - if (IsNotAllowedHost(searchUrl)) - return null; - - _onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {searchUrl}"); - string searchJson = await Http.Get(searchUrl, headers: headers, proxy: _proxyManager.Get()); - 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; - } - - public async Task> GetFundubs(int animeId) - { - string fundubsUrl = $"{_init.host}/api/player/{animeId}/translations"; - if (IsNotAllowedHost(fundubsUrl)) - return null; - - _onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {fundubsUrl}"); - string fundubsJson = await Http.Get(fundubsUrl, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(fundubsJson)) - return null; - - var fundubsResponse = JsonSerializer.Deserialize(fundubsJson); - if (fundubsResponse?.Translations == null || fundubsResponse.Translations.Count == 0) - return null; - - var fundubs = new List(); - foreach (var translation in fundubsResponse.Translations) - { - var fundubModel = new FundubModel - { - Fundub = translation.Translation, - Player = translation.Player - }; - fundubs.Add(fundubModel); - } - return fundubs; - } - - public async Task GetEpisodes(int animeId, int playerId, int fundubId) - { - string episodesUrl = $"{_init.host}/api/player/{animeId}/episodes?take=100&skip=-1&playerId={playerId}&translationId={fundubId}"; - if (IsNotAllowedHost(episodesUrl)) - return null; - - _onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {episodesUrl}"); - string episodesJson = await Http.Get(episodesUrl, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(episodesJson)) - return null; - - return JsonSerializer.Deserialize(episodesJson); - } - - public async Task ParseMoonAnimePage(string url) - { - try - { - string requestUrl = $"{url}?player=animeon.club"; - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", "https://animeon.club/") - }; - - if (IsNotAllowedHost(requestUrl)) - return null; - - _onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}"); - string html = await Http.Get(requestUrl, headers: headers, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(html)) - return null; - - var match = System.Text.RegularExpressions.Regex.Match(html, @"file:\s*""([^""]+\.m3u8)"""); - if (match.Success) - { - return match.Groups[1].Value; - } - } - catch (Exception ex) - { - _onLog($"AnimeON ParseMoonAnimePage error: {ex.Message}"); - } - - return null; - } - - public async Task ParseAshdiPage(string url) - { - try - { - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", "https://ashdi.vip/") - }; - - if (IsNotAllowedHost(url)) - return null; - - _onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {url}"); - string html = await Http.Get(url, headers: headers, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(html)) - return null; - - var match = System.Text.RegularExpressions.Regex.Match(html, @"file\s*:\s*['""]([^'""]+)['""]"); - if (match.Success) - { - return match.Groups[1].Value; - } - } - catch (Exception ex) - { - _onLog($"AnimeON ParseAshdiPage error: {ex.Message}"); - } - - return null; - } - - public async Task ResolveEpisodeStream(int episodeId) - { - try - { - string url = $"{_init.host}/api/player/{episodeId}/episode"; - if (IsNotAllowedHost(url)) - return null; - - _onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {url}"); - string json = await Http.Get(url, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(json)) - return null; - - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - if (root.TryGetProperty("fileUrl", out var fileProp)) - { - string fileUrl = fileProp.GetString(); - if (!string.IsNullOrEmpty(fileUrl)) - return fileUrl; - } - - if (root.TryGetProperty("videoUrl", out var videoProp)) - { - string videoUrl = videoProp.GetString(); - return await ResolveVideoUrl(videoUrl); - } - } - catch (Exception ex) - { - _onLog($"AnimeON ResolveEpisodeStream error: {ex.Message}"); - } - - return null; - } - - public async Task ResolveVideoUrl(string url) - { - if (string.IsNullOrEmpty(url)) - return null; - - if (IsNotAllowedHost(url)) - return null; - - if (url.Contains("moonanime.art")) - return await ParseMoonAnimePage(url); - - if (url.Contains("ashdi.vip/vod")) - return await ParseAshdiPage(url); - - return url; - } - - private static bool IsNotAllowedHost(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - return false; - - return NotAllowedHosts.Contains(uri.Host); - } - - public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) - { - if (init != null && init.rhub && rhub != -1) - return TimeSpan.FromMinutes(rhub); - - int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home; - if (ctime > multiaccess) - ctime = multiaccess; - - return TimeSpan.FromMinutes(ctime); - } - public async Task AggregateSerialStructure(int animeId, int season) - { - string memKey = $"AnimeON:aggregated:{animeId}:{season}"; - if (_hybridCache.TryGetValue(memKey, out AnimeON.Models.AnimeONAggregatedStructure cached)) - return cached; - - try - { - var structure = new AnimeON.Models.AnimeONAggregatedStructure - { - AnimeId = animeId, - Season = season, - Voices = new Dictionary() - }; - - var fundubs = await GetFundubs(animeId); - if (fundubs == null || fundubs.Count == 0) - return null; - - foreach (var fundub in fundubs) - { - if (fundub?.Fundub == null || fundub.Player == null) - continue; - - foreach (var player in fundub.Player) - { - string display = $"[{player.Name}] {fundub.Fundub.Name}"; - - var episodesData = await GetEpisodes(animeId, player.Id, fundub.Fundub.Id); - if (episodesData?.Episodes == null || episodesData.Episodes.Count == 0) - continue; - - var voiceInfo = new AnimeON.Models.AnimeONVoiceInfo - { - Name = fundub.Fundub.Name, - PlayerType = player.Name?.ToLower(), - DisplayName = display, - PlayerId = player.Id, - FundubId = fundub.Fundub.Id, - Episodes = episodesData.Episodes - .OrderBy(ep => ep.EpisodeNum) - .Select(ep => new AnimeON.Models.AnimeONEpisodeInfo - { - Number = ep.EpisodeNum, - Title = ep.Name, - Hls = ep.Hls, - VideoUrl = ep.VideoUrl, - EpisodeId = ep.Id - }) - .ToList() - }; - - structure.Voices[display] = voiceInfo; - } - } - - if (!structure.Voices.Any()) - return null; - - _hybridCache.Set(memKey, structure, cacheTime(20, init: _init)); - return structure; - } - catch (Exception ex) - { - _onLog?.Invoke($"AnimeON AggregateSerialStructure error: {ex.Message}"); - return null; - } - } - } -} diff --git a/AnimeON/Controller.cs b/AnimeON/Controller.cs deleted file mode 100644 index dc0e1c7..0000000 --- a/AnimeON/Controller.cs +++ /dev/null @@ -1,361 +0,0 @@ -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 - { - private static readonly HashSet NotAllowedHosts = - new HashSet( - new[] - { - "c3ZpdGFubW92aWU=", - "cG9ydGFsLXR2", - } - .Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))), - StringComparer.OrdinalIgnoreCase - ); - 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 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); - 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 season_tpl = new SeasonTpl(seasons.Count); - for (int i = 0; i < seasons.Count; i++) - { - 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()); - } - OnLog($"AnimeON: return seasons count={seasons.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) - return OnError("animeon", proxyManager); - - var selectedAnime = seasons[s]; - var structure = await invoke.AggregateSerialStructure(selectedAnime.Id, selectedAnime.Season); - if (structure == null || !structure.Voices.Any()) - return OnError("animeon", proxyManager); - - OnLog($"AnimeON: voices found = {structure.Voices.Count}"); - // Автовибір першої озвучки якщо t не задано - if (string.IsNullOrEmpty(t)) - t = structure.Voices.Keys.First(); - - // Формуємо список озвучок - var voice_tpl = new VoiceTpl(); - foreach (var voice in structure.Voices) - { - 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.Key, 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 = selectedAnime.Season.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.IsEmpty()) - 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> GetFundubs(OnlinesSettings init, int animeId) - { - string fundubsUrl = $"{init.host}/api/player/{animeId}/translations"; - if (IsNotAllowedHost(fundubsUrl)) - return null; - - 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); - if (fundubsResponse?.Translations == null || fundubsResponse.Translations.Count == 0) - return null; - - var fundubs = new List(); - foreach (var translation in fundubsResponse.Translations) - { - var fundubModel = new FundubModel - { - Fundub = translation.Translation, - Player = translation.Player - }; - fundubs.Add(fundubModel); - } - return fundubs; - } - - async Task 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}"; - if (IsNotAllowedHost(episodesUrl)) - return null; - - 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)}"; - if (IsNotAllowedHost(searchUrl)) - return null; - - 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; - } - - [HttpGet("animeon/play")] - public async Task Play(string url, int episode_id = 0, string title = null) - { - 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 streamHeaders = null; - bool forceProxy = false; - if (streamLink.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase)) - { - streamHeaders = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", "https://ashdi.vip/") - }; - forceProxy = true; - } - - string streamUrl = HostStreamProxy(init, accsArgs(streamLink), headers: streamHeaders, force_streamproxy: forceProxy); - string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? string.Empty}\"}}"; - OnLog("AnimeON Play: return call JSON"); - return Content(jsonResult, "application/json; charset=utf-8"); - } - - private static bool IsNotAllowedHost(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - return false; - - return NotAllowedHosts.Contains(uri.Host); - } - } -} diff --git a/AnimeON/ModInit.cs b/AnimeON/ModInit.cs deleted file mode 100644 index aefa2e5..0000000 --- a/AnimeON/ModInit.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Newtonsoft.Json; -using Shared; -using Shared.Engine; -using Newtonsoft.Json.Linq; -using Shared; -using Shared.Models.Online.Settings; -using Shared.Models.Module; - -using Newtonsoft.Json; -using Shared; -using Shared.Engine; -using Newtonsoft.Json.Linq; - -namespace AnimeON -{ - public class ModInit - { - public static OnlinesSettings AnimeON; - - /// - /// модуль загружен - /// - public static void loaded(InitspaceModel initspace) - { - AnimeON = new OnlinesSettings("AnimeON", "https://animeon.club", streamproxy: false, useproxy: false) - { - displayname = "🇯🇵 AnimeON", - displayindex = 0, - proxy = new Shared.Models.Base.ProxySettings() - { - useAuth = true, - username = "", - password = "", - list = new string[] { "socks5://ip:port" } - } - }; - AnimeON = ModuleInvoke.Conf("AnimeON", AnimeON).ToObject(); - - // Виводити "уточнити пошук" - AppInit.conf.online.with_search.Add("animeon"); - } - } -} \ No newline at end of file diff --git a/AnimeON/Models/AnimeONAggregatedStructure.cs b/AnimeON/Models/AnimeONAggregatedStructure.cs deleted file mode 100644 index d3f3f32..0000000 --- a/AnimeON/Models/AnimeONAggregatedStructure.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Collections.Generic; - -namespace AnimeON.Models -{ - /// Aggregated structure for AnimeON serial content to match Lampac standard navigation. - public class AnimeONAggregatedStructure - { - /// Anime identifier from AnimeON API. - public int AnimeId { get; set; } - - /// Season number. - public int Season { get; set; } - - /// Voices mapped by display key e.g. "[Moon] AniUA". - public Dictionary Voices { get; set; } = new Dictionary(); - } - - /// Voice information for a specific player/studio combination within a season. - public class AnimeONVoiceInfo - { - /// Studio/voice name (e.g., AniUA). - public string Name { get; set; } - - /// Player type ("moon" or "ashdi"). - public string PlayerType { get; set; } - - /// Display name (e.g., "[Moon] AniUA"). - public string DisplayName { get; set; } - - /// Player identifier from API. - public int PlayerId { get; set; } - - /// Fundub identifier from API. - public int FundubId { get; set; } - - /// Flat list of episodes for the selected season. - public List Episodes { get; set; } = new List(); - } - - /// Episode information within a voice. - public class AnimeONEpisodeInfo - { - /// Episode number. - public int Number { get; set; } - - /// Episode title. - public string Title { get; set; } - - /// Primary HLS link if available. - public string Hls { get; set; } - - /// Fallback video URL (iframe or direct). - public string VideoUrl { get; set; } - - /// Episode identifier from API. - public int EpisodeId { get; set; } - } -} \ No newline at end of file diff --git a/AnimeON/Models/EmbedModel.cs b/AnimeON/Models/EmbedModel.cs deleted file mode 100644 index afd9d3c..0000000 --- a/AnimeON/Models/EmbedModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace AnimeON.Models -{ - public class EmbedModel - { - [JsonPropertyName("translation")] - public string Translation { get; set; } - - [JsonPropertyName("links")] - public List<(string link, string quality)> Links { get; set; } - - [JsonPropertyName("subtitles")] - public Shared.Models.Templates.SubtitleTpl? Subtitles { get; set; } - - [JsonPropertyName("season")] - public int Season { get; set; } - - [JsonPropertyName("episode")] - public int Episode { get; set; } - } -} \ No newline at end of file diff --git a/AnimeON/Models/Models.cs b/AnimeON/Models/Models.cs deleted file mode 100644 index a498121..0000000 --- a/AnimeON/Models/Models.cs +++ /dev/null @@ -1,169 +0,0 @@ -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("translations")] - public List Translations { get; set; } - } - - public class TranslationModel - { - [JsonPropertyName("translation")] - public Fundub Translation { 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; } - - [JsonPropertyName("isSub")] - public bool IsSub { get; set; } - - [JsonPropertyName("synonyms")] - public List Synonyms { get; set; } - - [JsonPropertyName("studios")] - public List Studios { get; set; } - } - - public class Studio - { - [JsonPropertyName("slug")] - public string Slug { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("description")] - public string Description { get; set; } - - [JsonPropertyName("team")] - public object Team { get; set; } - - [JsonPropertyName("telegram")] - public string Telegram { get; set; } - - [JsonPropertyName("youtube")] - public string Youtube { get; set; } - - [JsonPropertyName("patreon")] - public string Patreon { get; set; } - - [JsonPropertyName("buymeacoffee")] - public string BuyMeACoffee { get; set; } - - [JsonPropertyName("avatar")] - public Avatar Avatar { get; set; } - } - - public class Avatar - { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("original")] - public string Original { get; set; } - - [JsonPropertyName("preview")] - public string Preview { get; set; } - } - - public class Player - { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("episodesCount")] - public int EpisodesCount { get; set; } - } - - public class FundubModel - { - public Fundub Fundub { get; set; } - public List Player { get; set; } - } - - public class EpisodeModel - { - [JsonPropertyName("episodes")] - public List Episodes { get; set; } - - [JsonPropertyName("anotherPlayer")] - public System.Text.Json.JsonElement AnotherPlayer { 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/Models/Serial.cs b/AnimeON/Models/Serial.cs deleted file mode 100644 index ed2392a..0000000 --- a/AnimeON/Models/Serial.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace AnimeON.Models -{ - public class Serial - { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("title_ua")] - public string TitleUa { get; set; } - - [JsonPropertyName("title_en")] - public string TitleEn { get; set; } - - [JsonPropertyName("year")] - public string Year { get; set; } - - [JsonPropertyName("imdb_id")] - public string ImdbId { get; set; } - - [JsonPropertyName("season")] - public int Season { get; set; } - - [JsonPropertyName("voices")] - public List Voices { get; set; } - } -} \ No newline at end of file diff --git a/AnimeON/Models/Voice.cs b/AnimeON/Models/Voice.cs deleted file mode 100644 index 4c25ce7..0000000 --- a/AnimeON/Models/Voice.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace AnimeON.Models -{ - public class Voice - { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("players")] - public List Players { get; set; } - } - - public class VoicePlayer - { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - } -} \ No newline at end of file diff --git a/AnimeON/OnlineApi.cs b/AnimeON/OnlineApi.cs deleted file mode 100644 index 4e164c4..0000000 --- a/AnimeON/OnlineApi.cs +++ /dev/null @@ -1,35 +0,0 @@ -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; - - // Визначаємо isAnime згідно стандарту Lampac (Deepwiki): - // isanime = true якщо original_language == "ja" або "zh" - bool hasLang = !string.IsNullOrEmpty(original_language); - bool isanime = hasLang && (original_language == "ja" || original_language == "zh"); - - // AnimeON — аніме-провайдер. Додаємо його: - // - при загальному пошуку (serial == -1), або - // - якщо контент визначений як аніме (isanime), або - // - якщо мова невідома (відсутній original_language) - if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang)) - { - string url = init.overridehost; - if (string.IsNullOrEmpty(url)) - url = $"{host}/animeon"; - - online.Add((init.displayname, url, "animeon", init.displayindex)); - } - - return online; - } - } -} diff --git a/AnimeON/manifest.json b/AnimeON/manifest.json deleted file mode 100644 index 86348fc..0000000 --- a/AnimeON/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "enable": true, - "version": 2, - "initspace": "AnimeON.ModInit", - "online": "AnimeON.OnlineApi" -} \ No newline at end of file diff --git a/Bamboo/Bamboo.csproj b/Bamboo/Bamboo.csproj deleted file mode 100644 index 1fbe365..0000000 --- a/Bamboo/Bamboo.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net9.0 - library - true - - - - - ..\..\Shared.dll - - - - diff --git a/Bamboo/BambooInvoke.cs b/Bamboo/BambooInvoke.cs deleted file mode 100644 index 8818135..0000000 --- a/Bamboo/BambooInvoke.cs +++ /dev/null @@ -1,355 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Web; -using Bamboo.Models; -using HtmlAgilityPack; -using Shared; -using Shared.Engine; -using Shared.Models; -using Shared.Models.Online.Settings; - -namespace Bamboo -{ - public class BambooInvoke - { - private static readonly HashSet NotAllowedHosts = - new HashSet( - new[] - { - "c3ZpdGFubW92aWU=", - "cG9ydGFsLXR2", - } - .Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))), - StringComparer.OrdinalIgnoreCase - ); - private readonly OnlinesSettings _init; - private readonly HybridCache _hybridCache; - private readonly Action _onLog; - private readonly ProxyManager _proxyManager; - - public BambooInvoke(OnlinesSettings init, HybridCache hybridCache, Action onLog, ProxyManager proxyManager) - { - _init = init; - _hybridCache = hybridCache; - _onLog = onLog; - _proxyManager = proxyManager; - } - - public async Task> Search(string title, string original_title) - { - string query = !string.IsNullOrEmpty(title) ? title : original_title; - if (string.IsNullOrEmpty(query)) - return null; - - string memKey = $"Bamboo:search:{query}"; - if (_hybridCache.TryGetValue(memKey, out List cached)) - return cached; - - try - { - string searchUrl = $"{_init.host}/index.php?do=search&subaction=search&story={HttpUtility.UrlEncode(query)}"; - if (IsNotAllowedHost(searchUrl)) - return null; - - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", _init.host) - }; - - _onLog?.Invoke($"Bamboo search: {searchUrl}"); - string html = await Http.Get(searchUrl, headers: headers, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(html)) - return null; - - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - var results = new List(); - var nodes = doc.DocumentNode.SelectNodes("//li[contains(@class,'slide-item')]"); - if (nodes != null) - { - foreach (var node in nodes) - { - string itemTitle = CleanText(node.SelectSingleNode(".//h6")?.InnerText); - string href = ExtractHref(node); - string poster = ExtractPoster(node); - - if (string.IsNullOrEmpty(itemTitle) || string.IsNullOrEmpty(href)) - continue; - - results.Add(new SearchResult - { - Title = itemTitle, - Url = href, - Poster = poster - }); - } - } - - if (results.Count > 0) - _hybridCache.Set(memKey, results, cacheTime(20, init: _init)); - - return results; - } - catch (Exception ex) - { - _onLog?.Invoke($"Bamboo search error: {ex.Message}"); - return null; - } - } - - public async Task GetSeriesEpisodes(string href) - { - if (string.IsNullOrEmpty(href)) - return null; - - string memKey = $"Bamboo:series:{href}"; - if (_hybridCache.TryGetValue(memKey, out SeriesEpisodes cached)) - return cached; - - try - { - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", _init.host) - }; - - if (IsNotAllowedHost(href)) - return null; - - _onLog?.Invoke($"Bamboo series page: {href}"); - string html = await Http.Get(href, headers: headers, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(html)) - return null; - - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - var result = new SeriesEpisodes(); - bool foundBlocks = false; - - var blocks = doc.DocumentNode.SelectNodes("//div[contains(@class,'mt-4')]"); - if (blocks != null) - { - foreach (var block in blocks) - { - string header = CleanText(block.SelectSingleNode(".//h3[contains(@class,'my-4')]")?.InnerText); - var episodes = ParseEpisodeSpans(block); - if (episodes.Count == 0) - continue; - - foundBlocks = true; - if (!string.IsNullOrEmpty(header) && header.Contains("Субтитри", StringComparison.OrdinalIgnoreCase)) - { - result.Sub.AddRange(episodes); - } - else if (!string.IsNullOrEmpty(header) && header.Contains("Озвучення", StringComparison.OrdinalIgnoreCase)) - { - result.Dub.AddRange(episodes); - } - } - } - - if (!foundBlocks || (result.Sub.Count == 0 && result.Dub.Count == 0)) - { - var fallback = ParseEpisodeSpans(doc.DocumentNode); - if (fallback.Count > 0) - result.Dub.AddRange(fallback); - } - - if (result.Sub.Count == 0) - { - var fallback = ParseEpisodeSpans(doc.DocumentNode); - if (fallback.Count > 0) - result.Sub.AddRange(fallback); - } - - _hybridCache.Set(memKey, result, cacheTime(30, init: _init)); - return result; - } - catch (Exception ex) - { - _onLog?.Invoke($"Bamboo series error: {ex.Message}"); - return null; - } - } - - public async Task> GetMovieStreams(string href) - { - if (string.IsNullOrEmpty(href)) - return null; - - string memKey = $"Bamboo:movie:{href}"; - if (_hybridCache.TryGetValue(memKey, out List cached)) - return cached; - - try - { - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", _init.host) - }; - - if (IsNotAllowedHost(href)) - return null; - - _onLog?.Invoke($"Bamboo movie page: {href}"); - string html = await Http.Get(href, headers: headers, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(html)) - return null; - - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - var streams = new List(); - var nodes = doc.DocumentNode.SelectNodes("//span[contains(@class,'mr-3') and @data-file]"); - if (nodes != null) - { - foreach (var node in nodes) - { - string dataFile = node.GetAttributeValue("data-file", ""); - if (string.IsNullOrEmpty(dataFile)) - continue; - - string title = node.GetAttributeValue("data-title", ""); - title = string.IsNullOrEmpty(title) ? CleanText(node.InnerText) : title; - - streams.Add(new StreamInfo - { - Title = title, - Url = NormalizeUrl(dataFile) - }); - } - } - - if (streams.Count > 0) - _hybridCache.Set(memKey, streams, cacheTime(30, init: _init)); - - return streams; - } - catch (Exception ex) - { - _onLog?.Invoke($"Bamboo movie error: {ex.Message}"); - return null; - } - } - - private List ParseEpisodeSpans(HtmlNode scope) - { - var episodes = new List(); - var nodes = scope.SelectNodes(".//span[@data-file]"); - if (nodes == null) - return episodes; - - foreach (var node in nodes) - { - string dataFile = node.GetAttributeValue("data-file", ""); - if (string.IsNullOrEmpty(dataFile)) - continue; - - string title = node.GetAttributeValue("data-title", ""); - if (string.IsNullOrEmpty(title)) - title = CleanText(node.InnerText); - - int? episodeNum = ExtractEpisodeNumber(title); - - episodes.Add(new EpisodeInfo - { - Title = string.IsNullOrEmpty(title) ? "Episode" : title, - Url = NormalizeUrl(dataFile), - Episode = episodeNum - }); - } - - return episodes; - } - - private string ExtractHref(HtmlNode node) - { - var link = node.SelectSingleNode(".//a[contains(@class,'hover-buttons')]") - ?? node.SelectSingleNode(".//a[@href]"); - if (link == null) - return string.Empty; - - string href = link.GetAttributeValue("href", ""); - return NormalizeUrl(href); - } - - private string ExtractPoster(HtmlNode node) - { - var img = node.SelectSingleNode(".//img"); - if (img == null) - return string.Empty; - - string src = img.GetAttributeValue("src", ""); - if (string.IsNullOrEmpty(src)) - src = img.GetAttributeValue("data-src", ""); - - return NormalizeUrl(src); - } - - private string NormalizeUrl(string url) - { - if (string.IsNullOrEmpty(url)) - return string.Empty; - - if (url.StartsWith("//")) - return IsNotAllowedHost($"https:{url}") ? string.Empty : $"https:{url}"; - - if (url.StartsWith("/")) - return IsNotAllowedHost(_init.host) ? string.Empty : $"{_init.host}{url}"; - - return IsNotAllowedHost(url) ? string.Empty : url; - } - - private static bool IsNotAllowedHost(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - return false; - - return NotAllowedHosts.Contains(uri.Host); - } - - private static int? ExtractEpisodeNumber(string title) - { - if (string.IsNullOrEmpty(title)) - return null; - - var match = Regex.Match(title, @"(\d+)"); - if (match.Success && int.TryParse(match.Groups[1].Value, out int value)) - return value; - - return null; - } - - private static string CleanText(string value) - { - if (string.IsNullOrEmpty(value)) - return string.Empty; - - return HtmlEntity.DeEntitize(value).Trim(); - } - - public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) - { - if (init != null && init.rhub && rhub != -1) - return TimeSpan.FromMinutes(rhub); - - int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home; - if (ctime > multiaccess) - ctime = multiaccess; - - return TimeSpan.FromMinutes(ctime); - } - } -} diff --git a/Bamboo/Controller.cs b/Bamboo/Controller.cs deleted file mode 100644 index 34365af..0000000 --- a/Bamboo/Controller.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Web; -using Bamboo.Models; -using Microsoft.AspNetCore.Mvc; -using Shared; -using Shared.Engine; -using Shared.Models; -using Shared.Models.Online.Settings; -using Shared.Models.Templates; - -namespace Bamboo.Controllers -{ - public class Controller : BaseOnlineController - { - ProxyManager proxyManager; - - public Controller() - { - proxyManager = new ProxyManager(ModInit.Bamboo); - } - - [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) - { - var init = await loadKit(ModInit.Bamboo); - if (!init.enable) - return Forbid(); - - var invoke = new BambooInvoke(init, hybridCache, OnLog, proxyManager); - - string itemUrl = href; - if (string.IsNullOrEmpty(itemUrl)) - { - var searchResults = await invoke.Search(title, original_title); - if (searchResults == null || searchResults.Count == 0) - return OnError("bamboo", proxyManager); - - if (searchResults.Count > 1) - { - var similar_tpl = new SimilarTpl(searchResults.Count); - foreach (var res in searchResults) - { - string link = $"{host}/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}"; - similar_tpl.Append(res.Title, string.Empty, string.Empty, link, res.Poster); - } - - return rjson ? Content(similar_tpl.ToJson(), "application/json; charset=utf-8") : Content(similar_tpl.ToHtml(), "text/html; charset=utf-8"); - } - - itemUrl = searchResults[0].Url; - } - - if (serial == 1) - { - var series = await invoke.GetSeriesEpisodes(itemUrl); - if (series == null || (series.Sub.Count == 0 && series.Dub.Count == 0)) - return OnError("bamboo", proxyManager); - - var voice_tpl = new VoiceTpl(); - var episode_tpl = new EpisodeTpl(); - - var availableVoices = new List<(string key, string name, List episodes)>(); - if (series.Sub.Count > 0) - availableVoices.Add(("sub", "Субтитри", series.Sub)); - if (series.Dub.Count > 0) - availableVoices.Add(("dub", "Озвучення", series.Dub)); - - if (string.IsNullOrEmpty(t)) - t = availableVoices.First().key; - - foreach (var voice in availableVoices) - { - string voiceLink = $"{host}/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&t={voice.key}&href={HttpUtility.UrlEncode(itemUrl)}"; - voice_tpl.Append(voice.name, voice.key == t, voiceLink); - } - - var selected = availableVoices.FirstOrDefault(v => v.key == t); - if (selected.episodes == null || selected.episodes.Count == 0) - return OnError("bamboo", proxyManager); - - int index = 1; - foreach (var ep in selected.episodes.OrderBy(e => e.Episode ?? int.MaxValue)) - { - int episodeNumber = ep.Episode ?? index; - string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {episodeNumber}" : ep.Title; - string streamUrl = HostStreamProxy(init, accsArgs(ep.Url)); - episode_tpl.Append(episodeName, title ?? original_title, "1", episodeNumber.ToString("D2"), streamUrl); - index++; - } - - 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 streams = await invoke.GetMovieStreams(itemUrl); - if (streams == null || streams.Count == 0) - return OnError("bamboo", proxyManager); - - var movie_tpl = new MovieTpl(title, original_title); - for (int i = 0; i < streams.Count; i++) - { - var stream = streams[i]; - string label = !string.IsNullOrEmpty(stream.Title) ? stream.Title : $"Варіант {i + 1}"; - string streamUrl = HostStreamProxy(init, accsArgs(stream.Url)); - movie_tpl.Append(label, streamUrl); - } - - return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8"); - } - } - } -} diff --git a/Bamboo/ModInit.cs b/Bamboo/ModInit.cs deleted file mode 100644 index d7994d5..0000000 --- a/Bamboo/ModInit.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Shared; -using Shared.Engine; -using Shared.Models.Online.Settings; -using Shared.Models.Module; - -namespace Bamboo -{ - public class ModInit - { - public static OnlinesSettings Bamboo; - - /// - /// модуль загружен - /// - public static void loaded(InitspaceModel initspace) - { - Bamboo = new OnlinesSettings("Bamboo", "https://bambooua.com", streamproxy: false, useproxy: false) - { - displayname = "BambooUA", - displayindex = 0, - proxy = new Shared.Models.Base.ProxySettings() - { - useAuth = true, - username = "", - password = "", - list = new string[] { "socks5://ip:port" } - } - }; - Bamboo = ModuleInvoke.Conf("Bamboo", Bamboo).ToObject(); - - // Виводити "уточнити пошук" - AppInit.conf.online.with_search.Add("bamboo"); - } - } -} diff --git a/Bamboo/Models/BambooModels.cs b/Bamboo/Models/BambooModels.cs deleted file mode 100644 index af99cb7..0000000 --- a/Bamboo/Models/BambooModels.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; - -namespace Bamboo.Models -{ - public class SearchResult - { - public string Title { get; set; } - public string Url { get; set; } - public string Poster { get; set; } - } - - public class EpisodeInfo - { - public string Title { get; set; } - public string Url { get; set; } - public int? Episode { get; set; } - } - - public class StreamInfo - { - public string Title { get; set; } - public string Url { get; set; } - } - - public class SeriesEpisodes - { - public List Sub { get; set; } = new(); - public List Dub { get; set; } = new(); - } -} diff --git a/Bamboo/OnlineApi.cs b/Bamboo/OnlineApi.cs deleted file mode 100644 index 5becaac..0000000 --- a/Bamboo/OnlineApi.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Shared.Models.Base; -using System.Collections.Generic; - -namespace Bamboo -{ - 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.Bamboo; - if (init.enable && !init.rip) - { - string url = init.overridehost; - if (string.IsNullOrEmpty(url)) - url = $"{host}/bamboo"; - - online.Add((init.displayname, url, "bamboo", init.displayindex)); - } - - return online; - } - } -} diff --git a/Bamboo/manifest.json b/Bamboo/manifest.json deleted file mode 100644 index 933863e..0000000 --- a/Bamboo/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "enable": true, - "version": 2, - "initspace": "Bamboo.ModInit", - "online": "Bamboo.OnlineApi" -} diff --git a/CikavaIdeya/CikavaIdeya.csproj b/CikavaIdeya/CikavaIdeya.csproj deleted file mode 100644 index 7befc43..0000000 --- a/CikavaIdeya/CikavaIdeya.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net9.0 - library - true - - - - - ..\..\Shared.dll - - - - \ No newline at end of file diff --git a/CikavaIdeya/CikavaIdeyaInvoke.cs b/CikavaIdeya/CikavaIdeyaInvoke.cs deleted file mode 100644 index 1fcb9c6..0000000 --- a/CikavaIdeya/CikavaIdeyaInvoke.cs +++ /dev/null @@ -1,396 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Shared; -using Shared.Models.Online.Settings; -using Shared.Models; -using System.Text.RegularExpressions; -using System.Text; -using HtmlAgilityPack; -using CikavaIdeya.Models; -using Shared.Engine; -using System.Linq; - -namespace CikavaIdeya -{ - public class CikavaIdeyaInvoke - { - private static readonly HashSet NotAllowedHosts = - new HashSet( - new[] - { - "c3ZpdGFubW92aWU=", - "cG9ydGFsLXR2", - } - .Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))), - StringComparer.OrdinalIgnoreCase - ); - private OnlinesSettings _init; - private HybridCache _hybridCache; - private Action _onLog; - private ProxyManager _proxyManager; - - public CikavaIdeyaInvoke(OnlinesSettings init, HybridCache hybridCache, Action onLog, ProxyManager proxyManager) - { - _init = init; - _hybridCache = hybridCache; - _onLog = onLog; - _proxyManager = proxyManager; - } - - public async Task> Search(string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool isfilm = false) - { - string filmTitle = !string.IsNullOrEmpty(title) ? title : original_title; - string memKey = $"CikavaIdeya:search:{filmTitle}:{year}:{isfilm}"; - if (_hybridCache.TryGetValue(memKey, out List res)) - return res; - - try - { - // Спочатку шукаємо по title - res = await PerformSearch(title, year); - - // Якщо нічого не знайдено і є original_title, шукаємо по ньому - if ((res == null || res.Count == 0) && !string.IsNullOrEmpty(original_title) && original_title != title) - { - _onLog($"No results for '{title}', trying search by original title '{original_title}'"); - res = await PerformSearch(original_title, year); - // Оновлюємо ключ кешу для original_title - if (res != null && res.Count > 0) - { - memKey = $"CikavaIdeya:search:{original_title}:{year}:{isfilm}"; - } - } - - if (res != null && res.Count > 0) - { - _hybridCache.Set(memKey, res, cacheTime(20)); - return res; - } - } - catch (Exception ex) - { - _onLog($"CikavaIdeya search error: {ex.Message}"); - } - return null; - } - - async Task> PerformSearch(string searchTitle, int year) - { - try - { - string searchUrl = $"{_init.host}/index.php?do=search&subaction=search&story={System.Web.HttpUtility.UrlEncode(searchTitle)}"; - var headers = new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }; - - if (IsNotAllowedHost(searchUrl)) - return null; - - var searchHtml = await Http.Get(searchUrl, headers: headers, proxy: _proxyManager.Get()); - // Перевіряємо, чи є результати пошуку - if (searchHtml.Contains("На жаль, пошук на сайті не дав жодних результатів")) - { - _onLog($"No search results for '{searchTitle}'"); - return new List(); - } - - var doc = new HtmlDocument(); - doc.LoadHtml(searchHtml); - - var filmNodes = doc.DocumentNode.SelectNodes("//div[@class='th-item']"); - if (filmNodes == null) - { - _onLog($"No film nodes found for '{searchTitle}'"); - return new List(); - } - - string filmUrl = null; - foreach (var filmNode in filmNodes) - { - var titleNode = filmNode.SelectSingleNode(".//div[@class='th-title']"); - if (titleNode == null || !titleNode.InnerText.Trim().ToLower().Contains(searchTitle.ToLower())) continue; - - var descNode = filmNode.SelectSingleNode(".//div[@class='th-subtitle']"); - if (year > 0 && (descNode?.InnerText ?? "").Contains(year.ToString())) - { - var linkNode = filmNode.SelectSingleNode(".//a[@class='th-in']"); - if (linkNode != null) - { - filmUrl = linkNode.GetAttributeValue("href", ""); - break; - } - } - } - - if (filmUrl == null) - { - var firstNode = filmNodes.FirstOrDefault()?.SelectSingleNode(".//a[@class='th-in']"); - if (firstNode != null) - filmUrl = firstNode.GetAttributeValue("href", ""); - } - - if (filmUrl == null) - { - _onLog($"No film URL found for '{searchTitle}'"); - return new List(); - } - - if (!filmUrl.StartsWith("http")) - filmUrl = _init.host + filmUrl; - - // Отримуємо список епізодів (для фільмів - один епізод, для серіалів - всі епізоди) - if (IsNotAllowedHost(filmUrl)) - return null; - - var filmHtml = await Http.Get(filmUrl, headers: headers, proxy: _proxyManager.Get()); - // Перевіряємо, чи не видалено контент - if (filmHtml.Contains("Видалено на прохання правовласника")) - { - _onLog($"Content removed on copyright holder request: {filmUrl}"); - return new List(); - } - - doc.LoadHtml(filmHtml); - - // Знаходимо JavaScript з даними про епізоди - var scriptNodes = doc.DocumentNode.SelectNodes("//script"); - if (scriptNodes != null) - { - foreach (var scriptNode in scriptNodes) - { - var scriptContent = scriptNode.InnerText; - if (scriptContent.Contains("switches = Object")) - { - _onLog($"Found switches script: {scriptContent}"); - // Парсимо структуру switches - var match = Regex.Match(scriptContent, @"switches = Object\((\{.*\})\);", RegexOptions.Singleline); - if (match.Success) - { - string switchesJson = match.Groups[1].Value; - _onLog($"Parsed switches JSON: {switchesJson}"); - // Спрощений парсинг JSON-подібної структури - var res = ParseSwitchesJson(switchesJson, _init.host, filmUrl); - _onLog($"Parsed episodes count: {res.Count}"); - foreach (var ep in res) - { - _onLog($"Episode: season={ep.season}, episode={ep.episode}, title={ep.title}, url={ep.url}"); - } - return res; - } - } - } - } - } - catch (Exception ex) - { - _onLog($"PerformSearch error for '{searchTitle}': {ex.Message}"); - } - return new List(); - } - - List ParseSwitchesJson(string json, string host, string baseUrl) - { - var result = new List(); - - try - { - _onLog($"Parsing switches JSON: {json}"); - // Спрощений парсинг JSON-подібної структури - // Приклад для серіалу: {"Player1":{"1 сезон":{"1 серія":"https://ashdi.vip/vod/57364",...},"2 сезон":{"1 серія":"https://ashdi.vip/vod/118170",...}}} - // Приклад для фільму: {"Player1":"https://ashdi.vip/vod/162246"} - - // Знаходимо плеєр Player1 - // Спочатку спробуємо знайти об'єкт Player1 - var playerObjectMatch = Regex.Match(json, @"""Player1""\s*:\s*(\{(?:[^{}]|(?\{)|(?<-open>\}))+(?(open)(?!)))", RegexOptions.Singleline); - if (playerObjectMatch.Success) - { - string playerContent = playerObjectMatch.Groups[1].Value; - _onLog($"Player1 object content: {playerContent}"); - - // Це серіал, парсимо сезони - var seasonMatches = Regex.Matches(playerContent, @"""([^""]+?сезон[^""]*?)""\s*:\s*\{((?:[^{}]|(?\{)|(?<-open>\}))+(?(open)(?!)))\}", RegexOptions.Singleline); - _onLog($"Found {seasonMatches.Count} seasons"); - foreach (Match seasonMatch in seasonMatches) - { - string seasonName = seasonMatch.Groups[1].Value; - string seasonContent = seasonMatch.Groups[2].Value; - _onLog($"Season: {seasonName}, Content: {seasonContent}"); - - // Витягуємо номер сезону - var seasonNumMatch = Regex.Match(seasonName, @"(\d+)"); - int seasonNum = seasonNumMatch.Success ? int.Parse(seasonNumMatch.Groups[1].Value) : 1; - _onLog($"Season number: {seasonNum}"); - - // Парсимо епізоди - var episodeMatches = Regex.Matches(seasonContent, @"""([^""]+?)""\s*:\s*""([^""]+?)""", RegexOptions.Singleline); - _onLog($"Found {episodeMatches.Count} episodes in season {seasonNum}"); - foreach (Match episodeMatch in episodeMatches) - { - string episodeName = episodeMatch.Groups[1].Value; - string episodeUrl = episodeMatch.Groups[2].Value; - _onLog($"Episode: {episodeName}, URL: {episodeUrl}"); - - // Витягуємо номер епізоду - var episodeNumMatch = Regex.Match(episodeName, @"(\d+)"); - int episodeNum = episodeNumMatch.Success ? int.Parse(episodeNumMatch.Groups[1].Value) : 1; - - result.Add(new CikavaIdeya.Models.EpisodeLinkInfo - { - url = episodeUrl, - title = episodeName, - season = seasonNum, - episode = episodeNum - }); - } - } - } - else - { - // Якщо не знайшли об'єкт, спробуємо знайти просте значення - var playerStringMatch = Regex.Match(json, @"""Player1""\s*:\s*(""([^""]+)"")", RegexOptions.Singleline); - if (playerStringMatch.Success) - { - string playerContent = playerStringMatch.Groups[1].Value; - _onLog($"Player1 string content: {playerContent}"); - - // Якщо це фільм (просте значення) - if (playerContent.StartsWith("\"") && playerContent.EndsWith("\"")) - { - string filmUrl = playerContent.Trim('"'); - result.Add(new CikavaIdeya.Models.EpisodeLinkInfo - { - url = filmUrl, - title = "Фільм", - season = 1, - episode = 1 - }); - } - } - else - { - _onLog("Player1 not found"); - } - } - } - catch (Exception ex) - { - _onLog($"ParseSwitchesJson error: {ex.Message}"); - } - - return result; - } - - public async Task ParseEpisode(string url) - { - var result = new CikavaIdeya.Models.PlayResult() { streams = new List<(string, string)>() }; - try - { - // Якщо це вже iframe URL (наприклад, з switches), повертаємо його - if (url.Contains("ashdi.vip")) - { - _onLog($"ParseEpisode: URL contains ashdi.vip, calling GetStreamUrlFromAshdi"); - string streamUrl = await GetStreamUrlFromAshdi(url); - _onLog($"ParseEpisode: GetStreamUrlFromAshdi returned {streamUrl}"); - if (!string.IsNullOrEmpty(streamUrl)) - { - result.streams.Add((streamUrl, "hls")); - _onLog($"ParseEpisode: added stream URL to result.streams"); - return result; - } - // Якщо не вдалося отримати посилання на поток, повертаємо iframe URL - _onLog($"ParseEpisode: stream URL is null or empty, setting iframe_url"); - result.iframe_url = url; - return result; - } - - // Інакше парсимо сторінку - if (IsNotAllowedHost(url)) - return result; - - string html = await Http.Get(url, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get()); - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - var iframe = doc.DocumentNode.SelectSingleNode("//div[@class='video-box']//iframe"); - if (iframe != null) - { - string iframeUrl = iframe.GetAttributeValue("src", "").Replace("&", "&"); - if (iframeUrl.StartsWith("//")) - iframeUrl = "https:" + iframeUrl; - - result.iframe_url = iframeUrl; - return result; - } - } - catch (Exception ex) - { - _onLog($"ParseEpisode error: {ex.Message}"); - } - return result; - } - public async Task GetStreamUrlFromAshdi(string url) - { - try - { - _onLog($"GetStreamUrlFromAshdi: trying to get stream URL from {url}"); - var headers = new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://ashdi.vip/") }; - if (IsNotAllowedHost(url)) - return null; - - string html = await Http.Get(url, headers: headers, proxy: _proxyManager.Get()); - _onLog($"GetStreamUrlFromAshdi: received HTML, length={html.Length}"); - - // Знаходимо JavaScript код з об'єктом player - var match = Regex.Match(html, @"var\s+player\s*=\s*new\s+Playerjs[\s\S]*?\(\s*({[\s\S]*?})\s*\)", RegexOptions.Multiline | RegexOptions.IgnoreCase); - if (match.Success) - { - _onLog($"GetStreamUrlFromAshdi: found player object"); - string playerJson = match.Groups[1].Value; - _onLog($"GetStreamUrlFromAshdi: playerJson={playerJson}"); - // Знаходимо поле file - var fileMatch = Regex.Match(playerJson, @"file\s*:\s*['""]([^'""]+)['""]", RegexOptions.Multiline | RegexOptions.IgnoreCase); - if (fileMatch.Success) - { - _onLog($"GetStreamUrlFromAshdi: found file URL: {fileMatch.Groups[1].Value}"); - return fileMatch.Groups[1].Value; - } - else - { - _onLog($"GetStreamUrlFromAshdi: file URL not found in playerJson"); - } - } - else - { - _onLog($"GetStreamUrlFromAshdi: player object not found in HTML"); - } - } - catch (Exception ex) - { - _onLog($"GetStreamUrlFromAshdi error: {ex.Message}"); - } - return null; - } - - private static bool IsNotAllowedHost(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - return false; - - return NotAllowedHosts.Contains(uri.Host); - } - - public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) - { - if (init != null && init.rhub && rhub != -1) - return TimeSpan.FromMinutes(rhub); - - int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home; - if (ctime > multiaccess) - ctime = multiaccess; - - return TimeSpan.FromMinutes(ctime); - } - } -} diff --git a/CikavaIdeya/Controller.cs b/CikavaIdeya/Controller.cs deleted file mode 100644 index cd1ca0c..0000000 --- a/CikavaIdeya/Controller.cs +++ /dev/null @@ -1,417 +0,0 @@ -using Shared.Engine; -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; -using System.Web; -using System.Linq; -using HtmlAgilityPack; -using Shared; -using Shared.Models.Templates; -using System.Text.RegularExpressions; -using System.Text; -using Shared.Models.Online.Settings; -using Shared.Models; -using CikavaIdeya.Models; - -namespace CikavaIdeya.Controllers -{ - public class Controller : BaseOnlineController - { - private static readonly HashSet NotAllowedHosts = - new HashSet( - new[] - { - "c3ZpdGFubW92aWU=", - "cG9ydGFsLXR2", - } - .Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))), - StringComparer.OrdinalIgnoreCase - ); - ProxyManager proxyManager; - - public Controller() - { - proxyManager = new ProxyManager(ModInit.CikavaIdeya); - } - - [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) - { - var init = await loadKit(ModInit.CikavaIdeya); - if (!init.enable) - return Forbid(); - - var invoke = new CikavaIdeyaInvoke(init, hybridCache, OnLog, 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"); - - if (play) - { - var episode = episodesInfo.FirstOrDefault(ep => ep.season == s && ep.episode == e); - if (serial == 0) // для фильма берем первый - episode = episodesInfo.FirstOrDefault(); - - if (episode == null) - return Content("CikavaIdeya", "text/html; charset=utf-8"); - - OnLog($"Controller: calling invoke.ParseEpisode with URL: {episode.url}"); - var playResult = await invoke.ParseEpisode(episode.url); - OnLog($"Controller: invoke.ParseEpisode returned playResult with streams.Count={playResult.streams?.Count ?? 0}, iframe_url={playResult.iframe_url}"); - - if (playResult.streams != null && playResult.streams.Count > 0) - { - string streamLink = playResult.streams.First().link; - string streamUrl = HostStreamProxy(init, accsArgs(streamLink)); - OnLog($"Controller: redirecting to stream URL: {streamUrl}"); - return Redirect(streamUrl); - } - - if (!string.IsNullOrEmpty(playResult.iframe_url)) - { - OnLog($"Controller: redirecting to iframe URL: {playResult.iframe_url}"); - // Для CikavaIdeya ми просто повертаємо iframe URL - return Redirect(playResult.iframe_url); - } - - if (playResult.streams != null && playResult.streams.Count > 0) - return Redirect(HostStreamProxy(init, accsArgs(playResult.streams.First().link))); - - return Content("CikavaIdeya", "text/html; charset=utf-8"); - } - - if (serial == 1) - { - if (s == -1) // Выбор сезона - { - var seasons = episodesInfo.GroupBy(ep => ep.season).ToDictionary(k => k.Key, v => v.ToList()); - OnLog($"Grouped seasons count: {seasons.Count}"); - foreach (var season in seasons) - { - OnLog($"Season {season.Key}: {season.Value.Count} episodes"); - } - var season_tpl = new SeasonTpl(seasons.Count); - foreach (var season in seasons.OrderBy(i => i.Key)) - { - string link = $"{host}/cikavaideya?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season.Key}"; - season_tpl.Append($"Сезон {season.Key}", link, $"{season.Key}"); - } - OnLog("Before generating season template HTML"); - string htmlContent = season_tpl.ToHtml(); - OnLog($"Season template HTML: {htmlContent}"); - return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(htmlContent, "text/html; charset=utf-8"); - } - - // Выбор эпизода - var episodes = episodesInfo.Where(ep => ep.season == s).OrderBy(ep => ep.episode).ToList(); - var movie_tpl = new MovieTpl(title, original_title, episodes.Count); - foreach(var ep in episodes) - { - string link = $"{host}/cikavaideya?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&e={ep.episode}&play=true"; - movie_tpl.Append(ep.title, accsArgs(link), method: "play"); - } - return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8"); - } - else // Фильм - { - string link = $"{host}/cikavaideya?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&play=true"; - var tpl = new MovieTpl(title, original_title, 1); - tpl.Append(title, accsArgs(link), method: "play"); - return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8"); - } - } - - async ValueTask> search(OnlinesSettings init, string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool isfilm = false) - { - string filmTitle = !string.IsNullOrEmpty(title) ? title : original_title; - string memKey = $"CikavaIdeya:search:{filmTitle}:{year}:{isfilm}"; - if (hybridCache.TryGetValue(memKey, out List res)) - return res; - - try - { - // Спочатку шукаємо по title - res = await PerformSearch(init, title, year); - - // Якщо нічого не знайдено і є original_title, шукаємо по ньому - if ((res == null || res.Count == 0) && !string.IsNullOrEmpty(original_title) && original_title != title) - { - OnLog($"No results for '{title}', trying search by original title '{original_title}'"); - res = await PerformSearch(init, original_title, year); - // Оновлюємо ключ кешу для original_title - if (res != null && res.Count > 0) - { - memKey = $"CikavaIdeya:search:{original_title}:{year}:{isfilm}"; - } - } - - if (res != null && res.Count > 0) - { - hybridCache.Set(memKey, res, cacheTime(20)); - return res; - } - } - catch (Exception ex) - { - OnLog($"CikavaIdeya search error: {ex.Message}"); - } - return null; - } - - async Task> PerformSearch(OnlinesSettings init, string searchTitle, int year) - { - try - { - string searchUrl = $"{init.host}/index.php?do=search&subaction=search&story={HttpUtility.UrlEncode(searchTitle)}"; - var headers = new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) }; - - if (IsNotAllowedHost(searchUrl)) - return null; - - var searchHtml = await Http.Get(searchUrl, headers: headers); - // Перевіряємо, чи є результати пошуку - if (searchHtml.Contains("На жаль, пошук на сайті не дав жодних результатів")) - { - OnLog($"No search results for '{searchTitle}'"); - return new List(); - } - - var doc = new HtmlDocument(); - doc.LoadHtml(searchHtml); - - var filmNodes = doc.DocumentNode.SelectNodes("//div[@class='th-item']"); - if (filmNodes == null) - { - OnLog($"No film nodes found for '{searchTitle}'"); - return new List(); - } - - string filmUrl = null; - foreach (var filmNode in filmNodes) - { - var titleNode = filmNode.SelectSingleNode(".//div[@class='th-title']"); - if (titleNode == null || !titleNode.InnerText.Trim().ToLower().Contains(searchTitle.ToLower())) continue; - - var descNode = filmNode.SelectSingleNode(".//div[@class='th-subtitle']"); - if (year > 0 && (descNode?.InnerText ?? "").Contains(year.ToString())) - { - var linkNode = filmNode.SelectSingleNode(".//a[@class='th-in']"); - if (linkNode != null) - { - filmUrl = linkNode.GetAttributeValue("href", ""); - break; - } - } - } - - if (filmUrl == null) - { - var firstNode = filmNodes.FirstOrDefault()?.SelectSingleNode(".//a[@class='th-in']"); - if (firstNode != null) - filmUrl = firstNode.GetAttributeValue("href", ""); - } - - if (filmUrl == null) - { - OnLog($"No film URL found for '{searchTitle}'"); - return new List(); - } - - if (!filmUrl.StartsWith("http")) - filmUrl = init.host + filmUrl; - - // Отримуємо список епізодів (для фільмів - один епізод, для серіалів - всі епізоди) - if (IsNotAllowedHost(filmUrl)) - return null; - - var filmHtml = await Http.Get(filmUrl, headers: headers); - // Перевіряємо, чи не видалено контент - if (filmHtml.Contains("Видалено на прохання правовласника")) - { - OnLog($"Content removed on copyright holder request: {filmUrl}"); - return new List(); - } - - doc.LoadHtml(filmHtml); - - // Знаходимо JavaScript з даними про епізоди - var scriptNodes = doc.DocumentNode.SelectNodes("//script"); - if (scriptNodes != null) - { - foreach (var scriptNode in scriptNodes) - { - var scriptContent = scriptNode.InnerText; - if (scriptContent.Contains("switches = Object")) - { - OnLog($"Found switches script: {scriptContent}"); - // Парсимо структуру switches - var match = Regex.Match(scriptContent, @"switches = Object\((\{.*\})\);", RegexOptions.Singleline); - if (match.Success) - { - string switchesJson = match.Groups[1].Value; - OnLog($"Parsed switches JSON: {switchesJson}"); - // Спрощений парсинг JSON-подібної структури - var res = ParseSwitchesJson(switchesJson, init.host, filmUrl); - OnLog($"Parsed episodes count: {res.Count}"); - foreach (var ep in res) - { - OnLog($"Episode: season={ep.season}, episode={ep.episode}, title={ep.title}, url={ep.url}"); - } - return res; - } - } - } - } - } - catch (Exception ex) - { - OnLog($"PerformSearch error for '{searchTitle}': {ex.Message}"); - } - return new List(); - } - - List ParseSwitchesJson(string json, string host, string baseUrl) - { - var result = new List(); - - try - { - OnLog($"Parsing switches JSON: {json}"); - // Спрощений парсинг JSON-подібної структури - // Приклад для серіалу: {"Player1":{"1 сезон":{"1 серія":"https://ashdi.vip/vod/57364",...},"2 сезон":{"1 серія":"https://ashdi.vip/vod/118170",...}}} - // Приклад для фільму: {"Player1":"https://ashdi.vip/vod/162246"} - - // Знаходимо плеєр Player1 - // Спочатку спробуємо знайти об'єкт Player1 - var playerObjectMatch = Regex.Match(json, @"""Player1""\s*:\s*(\{(?:[^{}]|(?\{)|(?<-open>\}))+(?(open)(?!)))", RegexOptions.Singleline); - if (playerObjectMatch.Success) - { - string playerContent = playerObjectMatch.Groups[1].Value; - OnLog($"Player1 object content: {playerContent}"); - - // Це серіал, парсимо сезони - var seasonMatches = Regex.Matches(playerContent, @"""([^""]+?сезон[^""]*?)""\s*:\s*\{((?:[^{}]|(?\{)|(?<-open>\}))+(?(open)(?!)))\}", RegexOptions.Singleline); - OnLog($"Found {seasonMatches.Count} seasons"); - foreach (Match seasonMatch in seasonMatches) - { - string seasonName = seasonMatch.Groups[1].Value; - string seasonContent = seasonMatch.Groups[2].Value; - OnLog($"Season: {seasonName}, Content: {seasonContent}"); - - // Витягуємо номер сезону - var seasonNumMatch = Regex.Match(seasonName, @"(\d+)"); - int seasonNum = seasonNumMatch.Success ? int.Parse(seasonNumMatch.Groups[1].Value) : 1; - OnLog($"Season number: {seasonNum}"); - - // Парсимо епізоди - var episodeMatches = Regex.Matches(seasonContent, @"""([^""]+?)""\s*:\s*""([^""]+?)""", RegexOptions.Singleline); - OnLog($"Found {episodeMatches.Count} episodes in season {seasonNum}"); - foreach (Match episodeMatch in episodeMatches) - { - string episodeName = episodeMatch.Groups[1].Value; - string episodeUrl = episodeMatch.Groups[2].Value; - OnLog($"Episode: {episodeName}, URL: {episodeUrl}"); - - // Витягуємо номер епізоду - var episodeNumMatch = Regex.Match(episodeName, @"(\d+)"); - int episodeNum = episodeNumMatch.Success ? int.Parse(episodeNumMatch.Groups[1].Value) : 1; - - result.Add(new EpisodeLinkInfo - { - url = episodeUrl, - title = episodeName, - season = seasonNum, - episode = episodeNum - }); - } - } - } - else - { - // Якщо не знайшли об'єкт, спробуємо знайти просте значення - var playerStringMatch = Regex.Match(json, @"""Player1""\s*:\s*(""([^""]+)"")", RegexOptions.Singleline); - if (playerStringMatch.Success) - { - string playerContent = playerStringMatch.Groups[1].Value; - OnLog($"Player1 string content: {playerContent}"); - - // Якщо це фільм (просте значення) - if (playerContent.StartsWith("\"") && playerContent.EndsWith("\"")) - { - string filmUrl = playerContent.Trim('"'); - result.Add(new EpisodeLinkInfo - { - url = filmUrl, - title = "Фільм", - season = 1, - episode = 1 - }); - } - } - else - { - OnLog("Player1 not found"); - } - } - } - catch (Exception ex) - { - OnLog($"ParseSwitchesJson error: {ex.Message}"); - } - - return result; - } - - async Task ParseEpisode(OnlinesSettings init, string url) - { - var result = new PlayResult() { streams = new List<(string, string)>() }; - try - { - // Якщо це вже iframe URL (наприклад, з switches), повертаємо його - if (url.Contains("ashdi.vip")) - { - result.iframe_url = url; - return result; - } - - // Інакше парсимо сторінку - if (IsNotAllowedHost(url)) - return result; - - string html = await Http.Get(url, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) }); - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - var iframe = doc.DocumentNode.SelectSingleNode("//div[@class='video-box']//iframe"); - if (iframe != null) - { - string iframeUrl = iframe.GetAttributeValue("src", "").Replace("&", "&"); - if (iframeUrl.StartsWith("//")) - iframeUrl = "https:" + iframeUrl; - - result.iframe_url = iframeUrl; - return result; - } - } - catch (Exception ex) - { - OnLog($"ParseEpisode error: {ex.Message}"); - } - return result; - } - - private static bool IsNotAllowedHost(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - return false; - - return NotAllowedHosts.Contains(uri.Host); - } - } -} diff --git a/CikavaIdeya/ModInit.cs b/CikavaIdeya/ModInit.cs deleted file mode 100644 index 678cb77..0000000 --- a/CikavaIdeya/ModInit.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Newtonsoft.Json; -using Shared; -using Shared.Engine; -using Newtonsoft.Json.Linq; -using Shared; -using Shared.Models.Online.Settings; -using Shared.Models.Module; - -using Newtonsoft.Json; -using Shared; -using Shared.Engine; -using Newtonsoft.Json.Linq; - -namespace CikavaIdeya -{ - public class ModInit - { - public static OnlinesSettings CikavaIdeya; - - /// - /// модуль загружен - /// - public static void loaded(InitspaceModel initspace) - { - CikavaIdeya = new OnlinesSettings("CikavaIdeya", "https://cikava-ideya.top", streamproxy: false, useproxy: false) - { - displayname = "ЦікаваІдея", - displayindex = 0, - proxy = new Shared.Models.Base.ProxySettings() - { - useAuth = true, - username = "a", - password = "a", - list = new string[] { "socks5://IP:PORT" } - } - }; - CikavaIdeya = ModuleInvoke.Conf("CikavaIdeya", CikavaIdeya).ToObject(); - - // Виводити "уточнити пошук" - AppInit.conf.online.with_search.Add("cikavaideya"); - } - } -} \ No newline at end of file diff --git a/CikavaIdeya/Models/EpisodeModel.cs b/CikavaIdeya/Models/EpisodeModel.cs deleted file mode 100644 index 46d0a11..0000000 --- a/CikavaIdeya/Models/EpisodeModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; - -namespace CikavaIdeya.Models -{ - public class EpisodeModel - { - [JsonPropertyName("episode_number")] - public int EpisodeNumber { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("url")] - public string Url { get; set; } - } -} \ No newline at end of file diff --git a/CikavaIdeya/Models/Models.cs b/CikavaIdeya/Models/Models.cs deleted file mode 100644 index 188475f..0000000 --- a/CikavaIdeya/Models/Models.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace CikavaIdeya.Models -{ - public class EpisodeLinkInfo - { - public string url { get; set; } - public string title { get; set; } - public int season { get; set; } - public int episode { get; set; } - } - - public class PlayResult - { - public string iframe_url { get; set; } - public List<(string link, string quality)> streams { get; set; } - } -} \ No newline at end of file diff --git a/CikavaIdeya/Models/PlayerModel.cs b/CikavaIdeya/Models/PlayerModel.cs deleted file mode 100644 index 3e871f9..0000000 --- a/CikavaIdeya/Models/PlayerModel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace CikavaIdeya.Models -{ - public class CikavaIdeyaPlayerModel - { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("qualities")] - public List<(string link, string quality)> Qualities { get; set; } - - [JsonPropertyName("subtitles")] - public Shared.Models.Templates.SubtitleTpl? Subtitles { get; set; } - } -} \ No newline at end of file diff --git a/CikavaIdeya/Models/SeasonModel.cs b/CikavaIdeya/Models/SeasonModel.cs deleted file mode 100644 index 570c7e0..0000000 --- a/CikavaIdeya/Models/SeasonModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace CikavaIdeya.Models -{ - public class SeasonModel - { - [JsonPropertyName("season_number")] - public int SeasonNumber { get; set; } - - [JsonPropertyName("episodes")] - public List Episodes { get; set; } - } -} \ No newline at end of file diff --git a/CikavaIdeya/OnlineApi.cs b/CikavaIdeya/OnlineApi.cs deleted file mode 100644 index 474fe1a..0000000 --- a/CikavaIdeya/OnlineApi.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Shared.Models.Base; -using System.Collections.Generic; - -namespace CikavaIdeya -{ - 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.CikavaIdeya; - - // Визначення isAnime згідно Lampac (Deepwiki): original_language == "ja" або "zh" - bool hasLang = !string.IsNullOrEmpty(original_language); - bool isanime = hasLang && (original_language == "ja" || original_language == "zh"); - - // CikavaIdeya — не-аніме провайдер. Додаємо якщо: - // - загальний пошук (serial == -1), або - // - контент НЕ аніме (!isanime), або - // - мова невідома (немає original_language) - if (init.enable && !init.rip && (serial == -1 || !isanime || !hasLang)) - { - string url = init.overridehost; - if (string.IsNullOrEmpty(url)) - url = $"{host}/cikavaideya"; - - online.Add((init.displayname, url, "cikavaideya", init.displayindex)); - } - - return online; - } - } -} \ No newline at end of file diff --git a/CikavaIdeya/manifest.json b/CikavaIdeya/manifest.json deleted file mode 100644 index f03df58..0000000 --- a/CikavaIdeya/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "enable": true, - "version": 2, - "initspace": "CikavaIdeya.ModInit", - "online": "CikavaIdeya.OnlineApi" -} \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md deleted file mode 100644 index 3df6fdf..0000000 --- a/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# Ukraine online source for Lampac - -- [x] AnimeON -- [x] [RIP] AniHUB -- [x] BambooUA -- [x] CikavaIdeya -- [x] StarLight -- [x] UAKino -- [x] UAFlix -- [x] UATuTFun -- [x] Unimay - - -## Installation - -1. Clone the repository: - ```bash - git clone https://github.com/lampac-ukraine/lampac-ukraine.git . - ``` - -2. Move the modules to the correct directory: - - If Lampac is installed system-wide, move the modules to the `module` directory. - - If Lampac is running in Docker, mount the volume: - ```bash - -v /path/to/your/cloned/repo/Uaflix:/home/module/Uaflix - ``` - -## Auto installation - -If Lampac version 148.1 and newer - -Create or update the module/repository.yaml file - -```YAML -- repository: https://github.com/lampame/lampac-ukraine - branch: main - modules: - - AnimeON - - Anihub - - Unimay - - CikavaIdeya - - Uaflix - - UaTUT - - Bamboo - - UAKino - - StarLight -``` - -branch - optional, default main - -modules - optional, if not specified, all modules from the repository will be installed - -## Init support - -```json -"Uaflix": { - "enable": true, - "domain": "https://uaflix.net", - "displayname": "Uaflix", - "streamproxy": false, - "useproxy": false, - "proxy": { - "useAuth": true, - "username": "FooBAR", - "password": "Strong_password", - "list": [ - "socks5://adress:port" - ] - }, - "displayindex": 1 - } -``` - -## Donate - -Support the author: https://lampame.donatik.me diff --git a/StarLight/Controller.cs b/StarLight/Controller.cs deleted file mode 100644 index 63dc423..0000000 --- a/StarLight/Controller.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Web; -using Microsoft.AspNetCore.Mvc; -using Shared; -using Shared.Engine; -using Shared.Models.Online.Settings; -using Shared.Models.Templates; -using StarLight.Models; - -namespace StarLight.Controllers -{ - public class Controller : BaseOnlineController - { - ProxyManager proxyManager; - - public Controller() - { - proxyManager = new ProxyManager(ModInit.StarLight); - } - - [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) - { - var init = await loadKit(ModInit.StarLight); - if (!init.enable) - return Forbid(); - - var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager); - - string itemUrl = href; - if (string.IsNullOrEmpty(itemUrl)) - { - var searchResults = await invoke.Search(title, original_title); - if (searchResults == null || searchResults.Count == 0) - return OnError("starlight", proxyManager); - - if (searchResults.Count > 1) - { - var similar_tpl = new SimilarTpl(searchResults.Count); - foreach (var res in searchResults) - { - string link = $"{host}/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Href)}"; - similar_tpl.Append(res.Title, string.Empty, string.Empty, link, string.Empty); - } - - return rjson ? Content(similar_tpl.ToJson(), "application/json; charset=utf-8") : Content(similar_tpl.ToHtml(), "text/html; charset=utf-8"); - } - - itemUrl = searchResults[0].Href; - } - - var project = await invoke.GetProject(itemUrl); - if (project == null) - return OnError("starlight", proxyManager); - - if (serial == 1 && project.Seasons.Count > 0) - { - if (s == -1) - { - var season_tpl = new SeasonTpl(project.Seasons.Count); - for (int i = 0; i < project.Seasons.Count; i++) - { - var seasonInfo = project.Seasons[i]; - string seasonName = string.IsNullOrEmpty(seasonInfo.Title) ? $"Сезон {i + 1}" : seasonInfo.Title; - string link = $"{host}/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={i}&href={HttpUtility.UrlEncode(itemUrl)}"; - 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 (s < 0 || s >= project.Seasons.Count) - return OnError("starlight", proxyManager); - - var season = project.Seasons[s]; - string seasonSlug = season.Slug; - var episodes = invoke.GetEpisodes(project, seasonSlug); - if (episodes == null || episodes.Count == 0) - return OnError("starlight", proxyManager); - - var episode_tpl = new EpisodeTpl(); - int index = 1; - string seasonNumber = GetSeasonNumber(season, s); - foreach (var ep in episodes) - { - if (string.IsNullOrEmpty(ep.Hash)) - continue; - - string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {index}" : ep.Title; - string callUrl = $"{host}/starlight/play?hash={HttpUtility.UrlEncode(ep.Hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}"; - episode_tpl.Append(episodeName, title ?? original_title, seasonNumber, index.ToString("D2"), accsArgs(callUrl), "call"); - index++; - } - - return rjson ? Content(episode_tpl.ToJson(), "application/json; charset=utf-8") : Content(episode_tpl.ToHtml(), "text/html; charset=utf-8"); - } - else - { - string hash = project.Hash; - if (string.IsNullOrEmpty(hash) && project.Episodes.Count > 0) - hash = project.Episodes.FirstOrDefault(e => !string.IsNullOrEmpty(e.Hash))?.Hash; - - if (string.IsNullOrEmpty(hash)) - return OnError("starlight", proxyManager); - - string callUrl = $"{host}/starlight/play?hash={HttpUtility.UrlEncode(hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}"; - var movie_tpl = new MovieTpl(title, original_title, 1); - movie_tpl.Append(string.IsNullOrEmpty(title) ? "StarLight" : title, accsArgs(callUrl), "call"); - - return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8"); - } - } - - [HttpGet] - [Route("starlight/play")] - async public Task Play(string hash, string title) - { - if (string.IsNullOrEmpty(hash)) - return OnError("starlight", proxyManager); - - var init = await loadKit(ModInit.StarLight); - if (!init.enable) - return Forbid(); - - var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager); - var result = await invoke.ResolveStream(hash); - if (result == null || string.IsNullOrEmpty(result.Stream)) - return OnError("starlight", proxyManager); - - string streamUrl = HostStreamProxy(init, accsArgs(result.Stream), proxy: proxyManager.Get()); - string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? result.Name ?? ""}\"}}"; - return Content(jsonResult, "application/json; charset=utf-8"); - } - - private static string GetSeasonNumber(SeasonInfo season, int fallbackIndex) - { - if (season?.Title == null) - return (fallbackIndex + 1).ToString(); - - var digits = new string(season.Title.Where(char.IsDigit).ToArray()); - return string.IsNullOrEmpty(digits) ? (fallbackIndex + 1).ToString() : digits; - } - } -} diff --git a/StarLight/ModInit.cs b/StarLight/ModInit.cs deleted file mode 100644 index ac0df4b..0000000 --- a/StarLight/ModInit.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Shared; -using Shared.Engine; -using Shared.Models.Online.Settings; -using Shared.Models.Module; - -namespace StarLight -{ - public class ModInit - { - public static OnlinesSettings StarLight; - - /// - /// модуль загружен - /// - public static void loaded(InitspaceModel initspace) - { - StarLight = new OnlinesSettings("StarLight", "https://tp-back.starlight.digital", streamproxy: false, useproxy: false) - { - displayname = "StarLight", - displayindex = 0, - proxy = new Shared.Models.Base.ProxySettings() - { - useAuth = true, - username = "", - password = "", - list = new string[] { "socks5://ip:port" } - } - }; - StarLight = ModuleInvoke.Conf("StarLight", StarLight).ToObject(); - - // Виводити "уточнити пошук" - AppInit.conf.online.with_search.Add("starlight"); - } - } -} diff --git a/StarLight/Models/StarLightModels.cs b/StarLight/Models/StarLightModels.cs deleted file mode 100644 index 90142aa..0000000 --- a/StarLight/Models/StarLightModels.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; - -namespace StarLight.Models -{ - public class SearchResult - { - public string Title { get; set; } - public string Type { get; set; } - public string Href { get; set; } - public string Channel { get; set; } - public string Project { get; set; } - } - - public class SeasonInfo - { - public string Title { get; set; } - public string Slug { get; set; } - } - - public class EpisodeInfo - { - public string Title { get; set; } - public string Hash { get; set; } - public string VideoSlug { get; set; } - public string Date { get; set; } - public string SeasonSlug { get; set; } - } - - public class ProjectInfo - { - public string Title { get; set; } - public string Description { get; set; } - public string Poster { get; set; } - public string Hash { get; set; } - public string Type { get; set; } - public string Channel { get; set; } - public List Seasons { get; set; } = new(); - public List Episodes { get; set; } = new(); - } - - public class StreamResult - { - public string Stream { get; set; } - public string Poster { get; set; } - public string Name { get; set; } - } -} diff --git a/StarLight/OnlineApi.cs b/StarLight/OnlineApi.cs deleted file mode 100644 index 20bb35b..0000000 --- a/StarLight/OnlineApi.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Shared.Models.Base; -using System; -using System.Collections.Generic; - -namespace StarLight -{ - 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)>(); - - if (!string.Equals(original_language, "uk", StringComparison.OrdinalIgnoreCase)) - return online; - - var init = ModInit.StarLight; - if (init.enable && !init.rip) - { - string url = init.overridehost; - if (string.IsNullOrEmpty(url)) - url = $"{host}/starlight"; - - online.Add((init.displayname, url, "starlight", init.displayindex)); - } - - return online; - } - } -} diff --git a/StarLight/StarLight.csproj b/StarLight/StarLight.csproj deleted file mode 100644 index 1fbe365..0000000 --- a/StarLight/StarLight.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net9.0 - library - true - - - - - ..\..\Shared.dll - - - - diff --git a/StarLight/StarLightInvoke.cs b/StarLight/StarLightInvoke.cs deleted file mode 100644 index 76c78ea..0000000 --- a/StarLight/StarLightInvoke.cs +++ /dev/null @@ -1,361 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using System.Web; -using Shared; -using Shared.Engine; -using Shared.Models; -using Shared.Models.Online.Settings; -using StarLight.Models; - -namespace StarLight -{ - public class StarLightInvoke - { - private const string PlayerApi = "https://vcms-api2.starlight.digital/player-api"; - private const string PlayerReferer = "https://teleportal.ua/"; - private const string Language = "ua"; - private static readonly HashSet NotAllowedHosts = - new HashSet( - new[] - { - "c3ZpdGFubW92aWU=", - "cG9ydGFsLXR2", - } - .Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))), - StringComparer.OrdinalIgnoreCase - ); - private readonly OnlinesSettings _init; - private readonly HybridCache _hybridCache; - private readonly Action _onLog; - private readonly ProxyManager _proxyManager; - - public StarLightInvoke(OnlinesSettings init, HybridCache hybridCache, Action onLog, ProxyManager proxyManager) - { - _init = init; - _hybridCache = hybridCache; - _onLog = onLog; - _proxyManager = proxyManager; - } - - public async Task> Search(string title, string original_title) - { - string query = !string.IsNullOrEmpty(title) ? title : original_title; - if (string.IsNullOrEmpty(query)) - return null; - - string memKey = $"StarLight:search:{query}"; - if (_hybridCache.TryGetValue(memKey, out List cached)) - return cached; - - string url = $"{_init.host}/{Language}/live-search?q={HttpUtility.UrlEncode(query)}"; - if (IsNotAllowedHost(url)) - return null; - - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", _init.host) - }; - - try - { - _onLog?.Invoke($"StarLight search: {url}"); - string payload = await Http.Get(url, headers: headers, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(payload)) - return null; - - var results = new List(); - using var document = JsonDocument.Parse(payload); - if (document.RootElement.ValueKind == JsonValueKind.Array) - { - foreach (var item in document.RootElement.EnumerateArray()) - { - string typeSlug = item.TryGetProperty("typeSlug", out var typeProp) ? typeProp.GetString() : null; - string channelSlug = item.TryGetProperty("channelSlug", out var channelProp) ? channelProp.GetString() : null; - string projectSlug = item.TryGetProperty("projectSlug", out var projectProp) ? projectProp.GetString() : null; - if (string.IsNullOrEmpty(typeSlug) || string.IsNullOrEmpty(channelSlug) || string.IsNullOrEmpty(projectSlug)) - continue; - - string href = $"{_init.host}/{Language}/{typeSlug}/{channelSlug}/{projectSlug}"; - results.Add(new SearchResult - { - Title = item.TryGetProperty("title", out var titleProp) ? titleProp.GetString() : null, - Type = typeSlug, - Href = href, - Channel = channelSlug, - Project = projectSlug - }); - } - } - - if (results.Count > 0) - _hybridCache.Set(memKey, results, cacheTime(15, init: _init)); - - return results; - } - catch (Exception ex) - { - _onLog?.Invoke($"StarLight search error: {ex.Message}"); - return null; - } - } - - public async Task GetProject(string href) - { - if (string.IsNullOrEmpty(href)) - return null; - - string memKey = $"StarLight:project:{href}"; - if (_hybridCache.TryGetValue(memKey, out ProjectInfo cached)) - return cached; - - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", _init.host) - }; - - try - { - if (IsNotAllowedHost(href)) - return null; - - _onLog?.Invoke($"StarLight project: {href}"); - string payload = await Http.Get(href, headers: headers, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(payload)) - return null; - - using var document = JsonDocument.Parse(payload); - var root = document.RootElement; - - var project = new ProjectInfo - { - Title = root.TryGetProperty("title", out var titleProp) ? titleProp.GetString() : null, - Description = root.TryGetProperty("description", out var descProp) ? descProp.GetString() : null, - Poster = NormalizeImage(root.TryGetProperty("image", out var imageProp) ? imageProp.GetString() : null), - Hash = root.TryGetProperty("hash", out var hashProp) ? hashProp.GetString() : null, - Type = root.TryGetProperty("typeSlug", out var typeProp) ? typeProp.GetString() : null, - Channel = root.TryGetProperty("channelTitle", out var channelProp) ? channelProp.GetString() : null - }; - - if (root.TryGetProperty("seasons", out var seasonsListProp) && seasonsListProp.ValueKind == JsonValueKind.Array) - { - foreach (var seasonItem in seasonsListProp.EnumerateArray()) - { - string seasonTitle = seasonItem.TryGetProperty("title", out var sTitle) ? sTitle.GetString() : null; - string seasonSlug = seasonItem.TryGetProperty("seasonSlug", out var sSlug) ? sSlug.GetString() : null; - AddSeason(project, seasonTitle, seasonSlug); - } - } - - if (root.TryGetProperty("seasonsGallery", out var seasonsProp) && seasonsProp.ValueKind == JsonValueKind.Array) - { - foreach (var seasonItem in seasonsProp.EnumerateArray()) - { - string seasonTitle = seasonItem.TryGetProperty("title", out var sTitle) ? sTitle.GetString() : null; - string seasonSlug = seasonItem.TryGetProperty("seasonSlug", out var sSlug) ? sSlug.GetString() : null; - AddSeason(project, seasonTitle, seasonSlug); - - if (seasonItem.TryGetProperty("items", out var itemsProp) && itemsProp.ValueKind == JsonValueKind.Array) - { - foreach (var item in itemsProp.EnumerateArray()) - { - project.Episodes.Add(new EpisodeInfo - { - Title = item.TryGetProperty("title", out var eTitle) ? eTitle.GetString() : null, - Hash = item.TryGetProperty("hash", out var eHash) ? eHash.GetString() : null, - VideoSlug = item.TryGetProperty("videoSlug", out var eSlug) ? eSlug.GetString() : null, - Date = item.TryGetProperty("dateOfBroadcast", out var eDate) ? eDate.GetString() : (item.TryGetProperty("timeUploadVideo", out var eDate2) ? eDate2.GetString() : null), - SeasonSlug = seasonSlug - }); - } - } - } - } - - await LoadMissingSeasonEpisodes(project, href, headers); - - _hybridCache.Set(memKey, project, cacheTime(10, init: _init)); - return project; - } - catch (Exception ex) - { - _onLog?.Invoke($"StarLight project error: {ex.Message}"); - return null; - } - } - - private async Task LoadMissingSeasonEpisodes(ProjectInfo project, string href, List headers) - { - if (project == null || string.IsNullOrEmpty(href)) - return; - - var missing = project.Seasons - .Where(s => !string.IsNullOrEmpty(s.Slug)) - .Where(s => !project.Episodes.Any(e => e.SeasonSlug == s.Slug)) - .ToList(); - - foreach (var seasonInfo in missing) - { - string seasonUrl = $"{href}/{seasonInfo.Slug}"; - try - { - _onLog?.Invoke($"StarLight season: {seasonUrl}"); - string payload = await Http.Get(seasonUrl, headers: headers, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(payload)) - continue; - - using var document = JsonDocument.Parse(payload); - var root = document.RootElement; - - if (root.TryGetProperty("items", out var itemsProp) && itemsProp.ValueKind == JsonValueKind.Array) - { - foreach (var item in itemsProp.EnumerateArray()) - { - string hash = item.TryGetProperty("hash", out var eHash) ? eHash.GetString() : null; - if (string.IsNullOrEmpty(hash)) - continue; - - if (project.Episodes.Any(e => e.SeasonSlug == seasonInfo.Slug && e.Hash == hash)) - continue; - - project.Episodes.Add(new EpisodeInfo - { - Title = item.TryGetProperty("title", out var eTitle) ? eTitle.GetString() : null, - Hash = hash, - VideoSlug = item.TryGetProperty("videoSlug", out var eSlug) ? eSlug.GetString() : null, - Date = item.TryGetProperty("dateOfBroadcast", out var eDate) ? eDate.GetString() : (item.TryGetProperty("timeUploadVideo", out var eDate2) ? eDate2.GetString() : null), - SeasonSlug = seasonInfo.Slug - }); - } - } - } - catch (Exception ex) - { - _onLog?.Invoke($"StarLight season error: {ex.Message}"); - } - } - } - - private static void AddSeason(ProjectInfo project, string title, string slug) - { - if (project == null || string.IsNullOrEmpty(slug)) - return; - - if (project.Seasons.Any(s => s.Slug == slug)) - return; - - project.Seasons.Add(new SeasonInfo { Title = title, Slug = slug }); - } - - public List GetEpisodes(ProjectInfo project, string seasonSlug) - { - if (project == null || project.Seasons.Count == 0) - return new List(); - - if (string.IsNullOrEmpty(seasonSlug)) - return project.Episodes; - - return project.Episodes.Where(e => e.SeasonSlug == seasonSlug).ToList(); - } - - public async Task ResolveStream(string hash) - { - if (string.IsNullOrEmpty(hash)) - return null; - - string url = $"{PlayerApi}/{hash}?referer={HttpUtility.UrlEncode(PlayerReferer)}&lang={Language}"; - if (IsNotAllowedHost(url)) - return null; - - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", PlayerReferer) - }; - - try - { - _onLog?.Invoke($"StarLight stream: {url}"); - string payload = await Http.Get(url, headers: headers, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(payload)) - return null; - - using var document = JsonDocument.Parse(payload); - var root = document.RootElement; - - string stream = null; - if (root.TryGetProperty("video", out var videoProp) && videoProp.ValueKind == JsonValueKind.Array) - { - var video = videoProp.EnumerateArray().FirstOrDefault(); - if (video.ValueKind != JsonValueKind.Undefined) - { - if (video.TryGetProperty("mediaHlsNoAdv", out var hlsNoAdv)) - stream = hlsNoAdv.GetString(); - if (string.IsNullOrEmpty(stream) && video.TryGetProperty("mediaHls", out var hls)) - stream = hls.GetString(); - if (string.IsNullOrEmpty(stream) && video.TryGetProperty("media", out var mediaProp) && mediaProp.ValueKind == JsonValueKind.Array) - { - var media = mediaProp.EnumerateArray().FirstOrDefault(); - if (media.TryGetProperty("url", out var mediaUrl)) - stream = mediaUrl.GetString(); - } - } - } - - if (string.IsNullOrEmpty(stream)) - return null; - - return new StreamResult - { - Stream = stream, - Poster = root.TryGetProperty("poster", out var posterProp) ? posterProp.GetString() : null, - Name = root.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null - }; - } - catch (Exception ex) - { - _onLog?.Invoke($"StarLight stream error: {ex.Message}"); - return null; - } - } - - private string NormalizeImage(string path) - { - if (string.IsNullOrEmpty(path)) - return string.Empty; - - if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - return IsNotAllowedHost(path) ? string.Empty : path; - - return IsNotAllowedHost(_init.host) ? string.Empty : $"{_init.host}{path}"; - } - - private static bool IsNotAllowedHost(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - return false; - - return NotAllowedHosts.Contains(uri.Host); - } - - public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) - { - if (init != null && init.rhub && rhub != -1) - return TimeSpan.FromMinutes(rhub); - - int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home; - if (ctime > multiaccess) - ctime = multiaccess; - - return TimeSpan.FromMinutes(ctime); - } - } -} diff --git a/StarLight/manifest.json b/StarLight/manifest.json deleted file mode 100644 index 1b214ae..0000000 --- a/StarLight/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "enable": true, - "version": 2, - "initspace": "StarLight.ModInit", - "online": "StarLight.OnlineApi" -} diff --git a/UAKino/Controller.cs b/UAKino/Controller.cs deleted file mode 100644 index 811c04b..0000000 --- a/UAKino/Controller.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Web; -using Microsoft.AspNetCore.Mvc; -using Shared; -using Shared.Engine; -using Shared.Models.Online.Settings; -using Shared.Models.Templates; -using UAKino.Models; - -namespace UAKino.Controllers -{ - public class Controller : BaseOnlineController - { - ProxyManager proxyManager; - - public Controller() - { - proxyManager = new ProxyManager(ModInit.UAKino); - } - - [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) - { - var init = await loadKit(ModInit.UAKino); - if (!init.enable) - return Forbid(); - - var invoke = new UAKinoInvoke(init, hybridCache, OnLog, proxyManager); - - string itemUrl = href; - if (string.IsNullOrEmpty(itemUrl)) - { - var searchResults = await invoke.Search(title, original_title, serial); - if (searchResults == null || searchResults.Count == 0) - return OnError("uakino", proxyManager); - - if (searchResults.Count > 1) - { - var similar_tpl = new SimilarTpl(searchResults.Count); - foreach (var res in searchResults) - { - string link = $"{host}/uakino?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}"; - similar_tpl.Append(res.Title, string.Empty, string.Empty, link, res.Poster); - } - - return rjson ? Content(similar_tpl.ToJson(), "application/json; charset=utf-8") : Content(similar_tpl.ToHtml(), "text/html; charset=utf-8"); - } - - itemUrl = searchResults[0].Url; - } - - if (serial == 1) - { - var playlist = await invoke.GetPlaylist(itemUrl); - if (playlist == null || playlist.Count == 0) - return OnError("uakino", proxyManager); - - var voiceGroups = playlist - .GroupBy(p => string.IsNullOrEmpty(p.Voice) ? "Основне" : p.Voice) - .Select(g => (key: g.Key, episodes: g.ToList())) - .ToList(); - - if (voiceGroups.Count == 0) - return OnError("uakino", proxyManager); - - if (string.IsNullOrEmpty(t)) - t = voiceGroups.First().key; - - var voice_tpl = new VoiceTpl(); - foreach (var voice in voiceGroups) - { - string voiceLink = $"{host}/uakino?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&t={HttpUtility.UrlEncode(voice.key)}&href={HttpUtility.UrlEncode(itemUrl)}"; - voice_tpl.Append(voice.key, voice.key == t, voiceLink); - } - - var selected = voiceGroups.FirstOrDefault(v => v.key == t); - if (selected.episodes == null || selected.episodes.Count == 0) - return OnError("uakino", proxyManager); - - var episode_tpl = new EpisodeTpl(); - int index = 1; - foreach (var ep in selected.episodes.OrderBy(e => UAKinoInvoke.TryParseEpisodeNumber(e.Title) ?? int.MaxValue)) - { - 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"); - index++; - } - - 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 - { - string playerUrl = await invoke.GetPlayerUrl(itemUrl); - if (string.IsNullOrEmpty(playerUrl)) - { - var playlist = await invoke.GetPlaylist(itemUrl); - playerUrl = playlist?.FirstOrDefault()?.Url; - } - - if (string.IsNullOrEmpty(playerUrl)) - return OnError("uakino", proxyManager); - - var movie_tpl = new MovieTpl(title, original_title); - string callUrl = $"{host}/uakino/play?url={HttpUtility.UrlEncode(playerUrl)}&title={HttpUtility.UrlEncode(title ?? original_title)}"; - movie_tpl.Append(string.IsNullOrEmpty(title) ? "UAKino" : title, accsArgs(callUrl), "call"); - - return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8"); - } - } - - [HttpGet] - [Route("uakino/play")] - async public Task Play(string url, string title) - { - if (string.IsNullOrEmpty(url)) - return OnError("uakino", proxyManager); - - var init = await loadKit(ModInit.UAKino); - if (!init.enable) - return Forbid(); - - var invoke = new UAKinoInvoke(init, hybridCache, OnLog, proxyManager); - var result = await invoke.ParsePlayer(url); - if (result == null || string.IsNullOrEmpty(result.File)) - return OnError("uakino", proxyManager); - - string streamUrl = HostStreamProxy(init, accsArgs(result.File), proxy: proxyManager.Get()); - string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? ""}\"}}"; - return Content(jsonResult, "application/json; charset=utf-8"); - } - } -} diff --git a/UAKino/ModInit.cs b/UAKino/ModInit.cs deleted file mode 100644 index 3df537f..0000000 --- a/UAKino/ModInit.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Shared; -using Shared.Engine; -using Shared.Models.Online.Settings; -using Shared.Models.Module; - -namespace UAKino -{ - public class ModInit - { - public static OnlinesSettings UAKino; - - /// - /// модуль загружен - /// - public static void loaded(InitspaceModel initspace) - { - UAKino = new OnlinesSettings("UAKino", "https://uakino.best", streamproxy: false, useproxy: false) - { - displayname = "UAKino", - displayindex = 0, - proxy = new Shared.Models.Base.ProxySettings() - { - useAuth = true, - username = "", - password = "", - list = new string[] { "socks5://ip:port" } - } - }; - UAKino = ModuleInvoke.Conf("UAKino", UAKino).ToObject(); - - // Виводити "уточнити пошук" - AppInit.conf.online.with_search.Add("uakino"); - } - } -} diff --git a/UAKino/Models/UAKinoModels.cs b/UAKino/Models/UAKinoModels.cs deleted file mode 100644 index 4b2f6ea..0000000 --- a/UAKino/Models/UAKinoModels.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; - -namespace UAKino.Models -{ - public class SearchResult - { - public string Title { get; set; } - public string Url { get; set; } - public string Poster { get; set; } - public string Season { get; set; } - } - - public class PlaylistItem - { - public string Title { get; set; } - public string Url { get; set; } - public string Voice { get; set; } - } - - public class SubtitleInfo - { - public string Lang { get; set; } - public string Url { get; set; } - } - - public class PlayerResult - { - public string File { get; set; } - public List Subtitles { get; set; } = new(); - } -} diff --git a/UAKino/OnlineApi.cs b/UAKino/OnlineApi.cs deleted file mode 100644 index 7404676..0000000 --- a/UAKino/OnlineApi.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Shared.Models.Base; -using System.Collections.Generic; - -namespace UAKino -{ - 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.UAKino; - if (init.enable && !init.rip) - { - string url = init.overridehost; - if (string.IsNullOrEmpty(url)) - url = $"{host}/uakino"; - - online.Add((init.displayname, url, "uakino", init.displayindex)); - } - - return online; - } - } -} diff --git a/UAKino/UAKino.csproj b/UAKino/UAKino.csproj deleted file mode 100644 index 1fbe365..0000000 --- a/UAKino/UAKino.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net9.0 - library - true - - - - - ..\..\Shared.dll - - - - diff --git a/UAKino/UAKinoInvoke.cs b/UAKino/UAKinoInvoke.cs deleted file mode 100644 index 2725d5e..0000000 --- a/UAKino/UAKinoInvoke.cs +++ /dev/null @@ -1,509 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Security; -using System.Security.Authentication; -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using HtmlAgilityPack; -using Shared; -using Shared.Engine; -using Shared.Models; -using Shared.Models.Online.Settings; -using UAKino.Models; - -namespace UAKino -{ - public class UAKinoInvoke - { - private const string PlaylistPath = "/engine/ajax/playlists.php"; - private const string PlaylistField = "playlist"; - private const string BlacklistRegex = "(/news/)|(/franchise/)"; - private static readonly HashSet NotAllowedHosts = - new HashSet( - new[] - { - "c3ZpdGFubW92aWU=", - "cG9ydGFsLXR2", - } - .Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))), - StringComparer.OrdinalIgnoreCase - ); - private readonly OnlinesSettings _init; - private readonly HybridCache _hybridCache; - private readonly Action _onLog; - private readonly ProxyManager _proxyManager; - - public UAKinoInvoke(OnlinesSettings init, HybridCache hybridCache, Action onLog, ProxyManager proxyManager) - { - _init = init; - _hybridCache = hybridCache; - _onLog = onLog; - _proxyManager = proxyManager; - } - - public async Task> Search(string title, string original_title, int serial) - { - var queries = new List(); - if (!string.IsNullOrEmpty(title)) - queries.Add(title); - if (!string.IsNullOrEmpty(original_title) && !queries.Contains(original_title)) - queries.Add(original_title); - - if (queries.Count == 0) - return null; - - string memKey = $"UAKino:search:{string.Join("|", queries)}:{serial}"; - if (_hybridCache.TryGetValue(memKey, out List cached)) - return cached; - - var results = new List(); - var seen = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var query in queries) - { - try - { - string searchUrl = $"{_init.host}/index.php?do=search&subaction=search&story={HttpUtility.UrlEncode(query)}"; - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", _init.host) - }; - - _onLog?.Invoke($"UAKino search: {searchUrl}"); - string html = await GetString(searchUrl, headers); - if (string.IsNullOrEmpty(html)) - continue; - - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - var nodes = doc.DocumentNode.SelectNodes("//div[contains(@class,'movie-item') and contains(@class,'short-item')]"); - if (nodes == null) - continue; - - foreach (var node in nodes) - { - var titleNode = node.SelectSingleNode(".//a[contains(@class,'movie-title')] | .//a[contains(@class,'full-movie')]"); - string itemTitle = CleanText(titleNode?.InnerText); - string href = NormalizeUrl(titleNode?.GetAttributeValue("href", "")); - if (string.IsNullOrEmpty(itemTitle)) - { - var altTitle = node.SelectSingleNode(".//div[contains(@class,'full-movie-title')]"); - itemTitle = CleanText(altTitle?.InnerText); - } - - if (string.IsNullOrEmpty(itemTitle) || string.IsNullOrEmpty(href) || IsBlacklisted(href)) - continue; - - if (serial == 1 && !IsSeriesUrl(href)) - continue; - if (serial == 0 && !IsMovieUrl(href)) - continue; - - string seasonText = CleanText(node.SelectSingleNode(".//div[contains(@class,'full-season')]")?.InnerText); - if (!string.IsNullOrEmpty(seasonText) && !itemTitle.Contains(seasonText, StringComparison.OrdinalIgnoreCase)) - itemTitle = $"{itemTitle} ({seasonText})"; - - string poster = ExtractPoster(node); - - if (seen.Contains(href)) - continue; - seen.Add(href); - - results.Add(new SearchResult - { - Title = itemTitle, - Url = href, - Poster = poster, - Season = seasonText - }); - } - - if (results.Count > 0) - break; - } - catch (Exception ex) - { - _onLog?.Invoke($"UAKino search error: {ex.Message}"); - } - } - - if (results.Count > 0) - _hybridCache.Set(memKey, results, cacheTime(20, init: _init)); - - return results; - } - - public async Task> GetPlaylist(string href) - { - string newsId = ExtractNewsId(href); - if (string.IsNullOrEmpty(newsId)) - return null; - - string memKey = $"UAKino:playlist:{newsId}"; - if (_hybridCache.TryGetValue(memKey, out List cached)) - return cached; - - string url = BuildPlaylistUrl(newsId); - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", href ?? _init.host), - new HeadersModel("X-Requested-With", "XMLHttpRequest") - }; - - try - { - _onLog?.Invoke($"UAKino playlist: {url}"); - string payload = await GetString(url, headers); - if (string.IsNullOrEmpty(payload)) - return null; - - using var document = JsonDocument.Parse(payload); - if (!document.RootElement.TryGetProperty("success", out var successProp) || !successProp.GetBoolean()) - return null; - - if (!document.RootElement.TryGetProperty("response", out var responseProp)) - return null; - - string html = responseProp.GetString(); - if (string.IsNullOrEmpty(html)) - return null; - - var items = ParsePlaylistHtml(html); - if (items.Count > 0) - _hybridCache.Set(memKey, items, cacheTime(10, init: _init)); - - return items; - } - catch (Exception ex) - { - _onLog?.Invoke($"UAKino playlist error: {ex.Message}"); - return null; - } - } - - public async Task GetPlayerUrl(string href) - { - if (string.IsNullOrEmpty(href)) - return null; - - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", _init.host) - }; - - try - { - _onLog?.Invoke($"UAKino movie page: {href}"); - string html = await GetString(href, headers); - if (string.IsNullOrEmpty(html)) - return null; - - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - var iframe = doc.DocumentNode.SelectSingleNode("//iframe[@id='pre']"); - if (iframe == null) - return null; - - string src = iframe.GetAttributeValue("src", ""); - if (string.IsNullOrEmpty(src)) - src = iframe.GetAttributeValue("data-src", ""); - - return NormalizeUrl(src); - } - catch (Exception ex) - { - _onLog?.Invoke($"UAKino player url error: {ex.Message}"); - return null; - } - } - - public async Task ParsePlayer(string url) - { - if (string.IsNullOrEmpty(url)) - return null; - - if (LooksLikeDirectStream(url)) - { - return new PlayerResult { File = url }; - } - - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", _init.host) - }; - - try - { - _onLog?.Invoke($"UAKino parse player: {url}"); - string html = await GetString(url, headers); - if (string.IsNullOrEmpty(html)) - return null; - - string file = ExtractPlayerFile(html); - if (string.IsNullOrEmpty(file)) - return null; - - return new PlayerResult - { - File = NormalizeUrl(file), - Subtitles = ExtractSubtitles(html) - }; - } - catch (Exception ex) - { - _onLog?.Invoke($"UAKino parse player error: {ex.Message}"); - return null; - } - } - - private async Task GetString(string url, List headers, int timeoutSeconds = 15) - { - if (IsNotAllowedHost(url)) - return null; - - var handler = new SocketsHttpHandler - { - AllowAutoRedirect = true, - AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip | DecompressionMethods.Deflate, - SslOptions = new SslClientAuthenticationOptions - { - RemoteCertificateValidationCallback = (_, _, _, _) => true, - EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13 - } - }; - - var proxy = _proxyManager.Get(); - if (proxy != null) - { - handler.UseProxy = true; - handler.Proxy = proxy; - } - else - { - handler.UseProxy = false; - } - - using var client = new HttpClient(handler); - using var req = new HttpRequestMessage(HttpMethod.Get, url); - - if (headers != null) - { - foreach (var h in headers) - req.Headers.TryAddWithoutValidation(h.name, h.val); - } - - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds))); - using var response = await client.SendAsync(req, cts.Token).ConfigureAwait(false); - if (!response.IsSuccessStatusCode) - return null; - - return await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false); - } - - private List ParsePlaylistHtml(string html) - { - var items = new List(); - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - var nodes = doc.DocumentNode.SelectNodes("//li[@data-file]"); - if (nodes == null) - return items; - - foreach (var node in nodes) - { - string dataFile = node.GetAttributeValue("data-file", ""); - if (string.IsNullOrEmpty(dataFile)) - continue; - - string title = CleanText(node.InnerText); - string voice = node.GetAttributeValue("data-voice", ""); - - items.Add(new PlaylistItem - { - Title = string.IsNullOrEmpty(title) ? "Episode" : title, - Url = NormalizeUrl(dataFile), - Voice = voice - }); - } - - return items; - } - - private string BuildPlaylistUrl(string newsId) - { - long ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - return $"{_init.host}{PlaylistPath}?news_id={newsId}&xfield={PlaylistField}&time={ts}"; - } - - private static string ExtractNewsId(string href) - { - if (string.IsNullOrEmpty(href)) - return null; - - string tail = href.TrimEnd('/').Split('/').LastOrDefault(); - if (string.IsNullOrEmpty(tail)) - return null; - - string newsId = tail.Split('-')[0]; - return string.IsNullOrEmpty(newsId) ? null : newsId; - } - - private static string ExtractPlayerFile(string html) - { - var match = Regex.Match(html, "file\\s*:\\s*['\"]([^'\"]+)['\"]", RegexOptions.IgnoreCase); - if (match.Success) - { - string value = match.Groups[1].Value.Trim(); - if (!value.StartsWith("[", StringComparison.Ordinal)) - return value; - } - - var sourceMatch = Regex.Match(html, "]+src=['\"]([^'\"]+)['\"]", RegexOptions.IgnoreCase); - if (sourceMatch.Success) - return sourceMatch.Groups[1].Value; - - var m3u8Match = Regex.Match(html, "(https?://[^\"'\\s>]+\\.m3u8[^\"'\\s>]*)", RegexOptions.IgnoreCase); - if (m3u8Match.Success) - return m3u8Match.Groups[1].Value; - - return null; - } - - private List ExtractSubtitles(string html) - { - var subtitles = new List(); - var match = Regex.Match(html, "subtitle\\s*:\\s*['\"]([^'\"]+)['\"]", RegexOptions.IgnoreCase); - if (!match.Success) - return subtitles; - - string value = match.Groups[1].Value.Trim(); - if (string.IsNullOrEmpty(value)) - return subtitles; - - if (value.StartsWith("[", StringComparison.Ordinal) && value.Contains(']')) - { - int endIdx = value.LastIndexOf(']'); - string label = value.Substring(1, endIdx - 1).Trim(); - string url = value[(endIdx + 1)..].Trim(); - url = NormalizeUrl(url); - if (!string.IsNullOrEmpty(url)) - subtitles.Add(new SubtitleInfo { Lang = string.IsNullOrEmpty(label) ? "unknown" : label, Url = url }); - } - else if (value.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - subtitles.Add(new SubtitleInfo { Lang = "unknown", Url = value }); - } - - return subtitles; - } - - private string NormalizeUrl(string url) - { - if (string.IsNullOrEmpty(url)) - return string.Empty; - - if (url.StartsWith("//")) - return IsNotAllowedHost($"https:{url}") ? string.Empty : $"https:{url}"; - - if (url.StartsWith("/")) - return IsNotAllowedHost(_init.host) ? string.Empty : $"{_init.host}{url}"; - - return IsNotAllowedHost(url) ? string.Empty : url; - } - - private static bool LooksLikeDirectStream(string url) - { - return url.Contains(".m3u8", StringComparison.OrdinalIgnoreCase) || url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsBlacklisted(string url) - { - return Regex.IsMatch(url ?? string.Empty, BlacklistRegex, RegexOptions.IgnoreCase); - } - - private static bool IsNotAllowedHost(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - return false; - - return NotAllowedHosts.Contains(uri.Host); - } - - private static bool IsSeriesUrl(string url) - { - return url.Contains("/seriesss/") || url.Contains("/anime-series/") || url.Contains("/cartoonseries/"); - } - - private static bool IsMovieUrl(string url) - { - return url.Contains("/filmy/") || url.Contains("/anime-solo/") || url.Contains("/features/"); - } - - private string ExtractPoster(HtmlNode node) - { - var img = node.SelectSingleNode(".//img"); - if (img == null) - return string.Empty; - - string src = img.GetAttributeValue("src", ""); - if (string.IsNullOrEmpty(src)) - src = img.GetAttributeValue("data-src", ""); - - return NormalizeUrl(src); - } - - private static string CleanText(string value) - { - if (string.IsNullOrEmpty(value)) - return string.Empty; - - return HtmlEntity.DeEntitize(value).Trim(); - } - - private static int? ExtractEpisodeNumber(string title) - { - if (string.IsNullOrEmpty(title)) - return null; - - var match = Regex.Match(title, @"(\d+)"); - if (match.Success && int.TryParse(match.Groups[1].Value, out int number)) - return number; - - return null; - } - - public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) - { - if (init != null && init.rhub && rhub != -1) - return TimeSpan.FromMinutes(rhub); - - int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home; - if (ctime > multiaccess) - ctime = multiaccess; - - return TimeSpan.FromMinutes(ctime); - } - - public static int? TryParseEpisodeNumber(string title) - { - return ExtractEpisodeNumber(title); - } - } -} diff --git a/UAKino/manifest.json b/UAKino/manifest.json deleted file mode 100644 index d4cb417..0000000 --- a/UAKino/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "enable": true, - "version": 2, - "initspace": "UAKino.ModInit", - "online": "UAKino.OnlineApi" -} diff --git a/UaTUT/Controller.cs b/UaTUT/Controller.cs deleted file mode 100644 index 4aac662..0000000 --- a/UaTUT/Controller.cs +++ /dev/null @@ -1,424 +0,0 @@ -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 UaTUT.Models; -using System.Text.RegularExpressions; -using Shared.Models.Online.Settings; -using Shared.Models; - -namespace UaTUT -{ - [Route("uatut")] - public class UaTUTController : BaseOnlineController - { - ProxyManager proxyManager; - - public UaTUTController() - { - proxyManager = new ProxyManager(ModInit.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) - { - var init = await loadKit(ModInit.UaTUT); - if (!init.enable) - return OnError(); - - OnLog($"UaTUT: {title} (serial={serial}, s={s}, season={season}, t={t})"); - - var invoke = new UaTUTInvoke(init, hybridCache, OnLog, proxyManager); - - // Використовуємо кеш для пошуку, щоб уникнути дублювання запитів - string searchCacheKey = $"uatut:search:{imdb_id ?? original_title ?? title}"; - var searchResults = await InvokeCache>(searchCacheKey, TimeSpan.FromMinutes(10), async () => - { - return await invoke.Search(original_title ?? title, imdb_id); - }); - - if (searchResults == null || !searchResults.Any()) - { - OnLog("UaTUT: No search results found"); - return OnError(); - } - - if (serial == 1) - { - return await HandleSeries(searchResults, imdb_id, kinopoisk_id, title, original_title, year, s, season, t, rjson, invoke); - } - else - { - return await HandleMovie(searchResults, rjson, 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) - { - var init = ModInit.UaTUT; - - // Фільтруємо тільки серіали та аніме - var seriesResults = searchResults.Where(r => r.Category == "Серіал" || r.Category == "Аніме").ToList(); - - if (!seriesResults.Any()) - { - OnLog("UaTUT: No series found in search results"); - return OnError(); - } - - if (s == -1) // Крок 1: Відображення списку серіалів - { - var season_tpl = new SeasonTpl(); - for (int i = 0; i < seriesResults.Count; i++) - { - var series = seriesResults[i]; - string seasonName = $"{series.Title} ({series.Year})"; - 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={i}"; - season_tpl.Append(seasonName, link, i.ToString()); - } - - OnLog($"UaTUT: generated {seriesResults.Count} series options"); - return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8"); - } - else if (season == -1) // Крок 2: Відображення сезонів для вибраного серіалу - { - if (s >= seriesResults.Count) - return OnError(); - - var selectedSeries = seriesResults[s]; - - // Використовуємо кеш для уникнення повторних запитів - string cacheKey = $"uatut:player_data:{selectedSeries.Id}"; - var playerData = await InvokeCache(cacheKey, TimeSpan.FromMinutes(10), async () => - { - return await GetPlayerDataCached(selectedSeries, invoke); - }); - - if (playerData?.Voices == null || !playerData.Voices.Any()) - return OnError(); - - // Використовуємо першу озвучку для отримання списку сезонів - 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}/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()); - } - - OnLog($"UaTUT: found {firstVoice.Seasons.Count} seasons"); - return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8"); - } - else // Крок 3: Відображення озвучок та епізодів для вибраного сезону - { - if (s >= seriesResults.Count) - return OnError(); - - var selectedSeries = seriesResults[s]; - - // Використовуємо той самий кеш - string cacheKey = $"uatut:player_data:{selectedSeries.Id}"; - var playerData = await InvokeCache(cacheKey, TimeSpan.FromMinutes(10), async () => - { - return await GetPlayerDataCached(selectedSeries, invoke); - }); - - if (playerData?.Voices == null || !playerData.Voices.Any()) - return OnError(); - - // Перевіряємо чи існує вибраний сезон - if (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}/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}"; - 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]; - string episodeName = episode.Title; - string episodeFile = episode.File; - - 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"); - } - } - } - } - - int voiceCount = playerData.Voices.Count; - int episodeCount = 0; - if (!string.IsNullOrEmpty(selectedVoice) && int.TryParse(selectedVoice, out int vIndex) && vIndex < playerData.Voices.Count) - { - var selectedVoiceData = playerData.Voices[vIndex]; - if (season < selectedVoiceData.Seasons.Count) - { - episodeCount = selectedVoiceData.Seasons[season].Episodes.Count; - } - } - - OnLog($"UaTUT: 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"); - } - } - - // Допоміжний метод для кешованого отримання даних плеєра - private async Task GetPlayerDataCached(SearchResult selectedSeries, UaTUTInvoke invoke) - { - var pageContent = await invoke.GetMoviePageContent(selectedSeries.Id); - if (string.IsNullOrEmpty(pageContent)) - return null; - - var playerUrl = await invoke.GetPlayerUrl(pageContent); - if (string.IsNullOrEmpty(playerUrl)) - return null; - - return await invoke.GetPlayerData(playerUrl); - } - - // Допоміжний метод для витягування номера епізоду з назви - private int ExtractEpisodeNumber(string title) - { - if (string.IsNullOrEmpty(title)) - return 0; - - var match = Regex.Match(title, @"(\d+)"); - return match.Success ? int.Parse(match.Groups[1].Value) : 0; - } - - private async Task HandleMovie(List searchResults, bool rjson, UaTUTInvoke invoke) - { - var init = ModInit.UaTUT; - - // Фільтруємо тільки фільми - var movieResults = searchResults.Where(r => r.Category == "Фільм").ToList(); - - if (!movieResults.Any()) - { - OnLog("UaTUT: No movies found in search results"); - return OnError(); - } - - var movie_tpl = new MovieTpl(title: "UaTUT Movies", original_title: "UaTUT Movies"); - - foreach (var movie in movieResults) - { - var pageContent = await invoke.GetMoviePageContent(movie.Id); - if (string.IsNullOrEmpty(pageContent)) - continue; - - var playerUrl = await invoke.GetPlayerUrl(pageContent); - if (string.IsNullOrEmpty(playerUrl)) - continue; - - var playerData = await invoke.GetPlayerData(playerUrl); - if (playerData?.File == null) - continue; - - string movieName = $"{movie.Title} ({movie.Year})"; - string movieLink = $"{host}/uatut/play/movie?imdb_id={movie.Id}&title={HttpUtility.UrlEncode(movie.Title)}&year={movie.Year}"; - movie_tpl.Append(movieName, movieLink, "call"); - } - - if (movie_tpl.IsEmpty()) - { - OnLog("UaTUT: No playable movies found"); - return OnError(); - } - - OnLog($"UaTUT: found {movieResults.Count} movies"); - return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8"); - } - - [HttpGet] - [Route("play/movie")] - async public Task PlayMovie(long imdb_id, string title, int year, bool play = false, bool rjson = false) - { - var init = await loadKit(ModInit.UaTUT); - if (!init.enable) - return OnError(); - - OnLog($"UaTUT PlayMovie: {title} ({year}) play={play}"); - - var invoke = new UaTUTInvoke(init, hybridCache, OnLog, proxyManager); - - // Використовуємо кеш для пошуку - string searchCacheKey = $"uatut:search:{title}"; - var searchResults = await InvokeCache>(searchCacheKey, TimeSpan.FromMinutes(10), async () => - { - return await invoke.Search(title, null); - }); - - if (searchResults == null || !searchResults.Any()) - { - OnLog("UaTUT PlayMovie: No search results found"); - return OnError(); - } - - // Шукаємо фільм за ID - var movie = searchResults.FirstOrDefault(r => r.Id == imdb_id.ToString() && r.Category == "Фільм"); - if (movie == null) - { - OnLog("UaTUT PlayMovie: Movie not found"); - return OnError(); - } - - var pageContent = await invoke.GetMoviePageContent(movie.Id); - if (string.IsNullOrEmpty(pageContent)) - return OnError(); - - var playerUrl = await invoke.GetPlayerUrl(pageContent); - if (string.IsNullOrEmpty(playerUrl)) - return OnError(); - - var playerData = await invoke.GetPlayerData(playerUrl); - if (playerData?.File == null) - return OnError(); - - OnLog($"UaTUT PlayMovie: Found direct file: {playerData.File}"); - - string streamUrl = HostStreamProxy(init, playerData.File); - - // Якщо play=true, робимо Redirect, інакше повертаємо JSON - if (play) - return Redirect(streamUrl); - else - return Content(VideoTpl.ToJson("play", streamUrl, title), "application/json; charset=utf-8"); - } - - [HttpGet] - [Route("play")] - async public 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) - { - var init = await loadKit(ModInit.UaTUT); - if (!init.enable) - return OnError(); - - OnLog($"UaTUT Play: {title} (s={s}, season={season}, t={t}, episodeId={episodeId}) play={play}"); - - var invoke = new UaTUTInvoke(init, hybridCache, OnLog, proxyManager); - - // Використовуємо кеш для пошуку - string searchCacheKey = $"uatut:search:{imdb_id ?? original_title ?? title}"; - var searchResults = await InvokeCache>(searchCacheKey, TimeSpan.FromMinutes(10), async () => - { - return await invoke.Search(original_title ?? title, imdb_id); - }); - - if (searchResults == null || !searchResults.Any()) - { - OnLog("UaTUT Play: No search results found"); - return OnError(); - } - - // Фільтруємо тільки серіали та аніме - var seriesResults = searchResults.Where(r => r.Category == "Серіал" || r.Category == "Аніме").ToList(); - - if (!seriesResults.Any() || s >= seriesResults.Count) - { - OnLog("UaTUT Play: No series found or invalid series index"); - return OnError(); - } - - var selectedSeries = seriesResults[s]; - - // Використовуємо той самий кеш як і в HandleSeries - string cacheKey = $"uatut:player_data:{selectedSeries.Id}"; - var playerData = await InvokeCache(cacheKey, TimeSpan.FromMinutes(10), async () => - { - return await GetPlayerDataCached(selectedSeries, invoke); - }); - - if (playerData?.Voices == null || !playerData.Voices.Any()) - { - OnLog("UaTUT Play: No player data or voices found"); - return OnError(); - } - - // Знаходимо потрібний епізод в конкретному сезоні та озвучці - if (int.TryParse(t, out int voiceIndex) && voiceIndex < playerData.Voices.Count) - { - var selectedVoice = playerData.Voices[voiceIndex]; - - if (season >= 0 && season < selectedVoice.Seasons.Count) - { - var selectedSeasonData = selectedVoice.Seasons[season]; - - foreach (var episode in selectedSeasonData.Episodes) - { - if (episode.Id == episodeId && !string.IsNullOrEmpty(episode.File)) - { - OnLog($"UaTUT Play: Found episode {episode.Title}, stream: {episode.File}"); - - string streamUrl = HostStreamProxy(init, episode.File); - string episodeTitle = $"{title ?? original_title} - {episode.Title}"; - - // Якщо play=true, робимо Redirect, інакше повертаємо JSON - if (play) - return Redirect(streamUrl); - else - return Content(VideoTpl.ToJson("play", streamUrl, episodeTitle), "application/json; charset=utf-8"); - } - } - } - else - { - OnLog($"UaTUT Play: Invalid season {season}, available seasons: {selectedVoice.Seasons.Count}"); - } - } - else - { - OnLog($"UaTUT Play: Invalid voice index {t}, available voices: {playerData.Voices.Count}"); - } - - OnLog("UaTUT Play: Episode not found"); - return OnError(); - } - } -} diff --git a/UaTUT/ModInit.cs b/UaTUT/ModInit.cs deleted file mode 100644 index 25a6e13..0000000 --- a/UaTUT/ModInit.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Newtonsoft.Json; -using Shared; -using Shared.Engine; -using Newtonsoft.Json.Linq; -using Shared.Models.Online.Settings; -using Shared.Models.Module; - -namespace UaTUT -{ - public class ModInit - { - public static OnlinesSettings UaTUT; - - /// - /// модуль загружен - /// - public static void loaded(InitspaceModel initspace) - { - UaTUT = new OnlinesSettings("UaTUT", "https://uk.uatut.fun", streamproxy: false, useproxy: false) - { - displayname = "🇺🇦 UaTUT", - displayindex = 0, - apihost = "https://uk.uatut.fun/watch", - proxy = new Shared.Models.Base.ProxySettings() - { - useAuth = true, - username = "a", - password = "a", - list = new string[] { "socks5://IP:PORT" } - } - }; - UaTUT = ModuleInvoke.Conf("UaTUT", UaTUT).ToObject(); - - // Виводити "уточнити пошук" - AppInit.conf.online.with_search.Add("uatut"); - } - } -} diff --git a/UaTUT/Models/UaTUTModels.cs b/UaTUT/Models/UaTUTModels.cs deleted file mode 100644 index c9ea23d..0000000 --- a/UaTUT/Models/UaTUTModels.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace UaTUT.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/UaTUT/OnlineApi.cs b/UaTUT/OnlineApi.cs deleted file mode 100644 index ea4ed11..0000000 --- a/UaTUT/OnlineApi.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Shared.Models.Base; -using System.Collections.Generic; - -namespace UaTUT -{ - 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.UaTUT; - // UaTUT: змішаний контент (аніме + не-аніме) — завжди включати при enable && !rip - if (init.enable && !init.rip) - { - string url = init.overridehost; - if (string.IsNullOrEmpty(url)) - url = $"{host}/uatut"; - - online.Add((init.displayname, url, "uatut", init.displayindex)); - } - - return online; - } - } -} diff --git a/UaTUT/UaTUT.csproj b/UaTUT/UaTUT.csproj deleted file mode 100644 index 1fbe365..0000000 --- a/UaTUT/UaTUT.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net9.0 - library - true - - - - - ..\..\Shared.dll - - - - diff --git a/UaTUT/UaTUTInvoke.cs b/UaTUT/UaTUTInvoke.cs deleted file mode 100644 index aa2613c..0000000 --- a/UaTUT/UaTUTInvoke.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Web; -using Newtonsoft.Json; -using Shared.Engine; -using Shared.Models.Online.Settings; -using Shared.Models; -using UaTUT.Models; - -namespace UaTUT -{ - public class UaTUTInvoke - { - private static readonly HashSet NotAllowedHosts = - new HashSet( - new[] - { - "c3ZpdGFubW92aWU=", - "cG9ydGFsLXR2", - } - .Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))), - StringComparer.OrdinalIgnoreCase - ); - private OnlinesSettings _init; - private HybridCache _hybridCache; - private Action _onLog; - private ProxyManager _proxyManager; - - public UaTUTInvoke(OnlinesSettings init, HybridCache hybridCache, Action onLog, ProxyManager proxyManager) - { - _init = init; - _hybridCache = hybridCache; - _onLog = onLog; - _proxyManager = proxyManager; - } - - public async Task> Search(string query, string imdbId = null) - { - try - { - string searchUrl = $"{_init.apihost}/search.php"; - - // Поступовий пошук: спочатку по imdbId, потім по назві - 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($"UaTUT Search error: {ex.Message}"); - return new List(); - } - } - - private async Task> PerformSearch(string searchUrl, string query) - { - string url = $"{searchUrl}?q={HttpUtility.UrlEncode(query)}"; - _onLog($"UaTUT searching: {url}"); - - if (IsNotAllowedHost(url)) - return null; - - var headers = new List() { new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") }; - var response = await Http.Get(url, headers: headers, proxy: _proxyManager.Get()); - - if (string.IsNullOrEmpty(response)) - return null; - - try - { - var results = JsonConvert.DeserializeObject>(response); - _onLog($"UaTUT found {results?.Count ?? 0} results for query: {query}"); - return results; - } - catch (Exception ex) - { - _onLog($"UaTUT parse error: {ex.Message}"); - return null; - } - } - - public async Task GetMoviePageContent(string movieId) - { - try - { - string url = $"{_init.apihost}/{movieId}"; - _onLog($"UaTUT getting movie page: {url}"); - - if (IsNotAllowedHost(url)) - return null; - - var headers = new List() { new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") }; - var response = await Http.Get(url, headers: headers, proxy: _proxyManager.Get()); - - return response; - } - catch (Exception ex) - { - _onLog($"UaTUT GetMoviePageContent error: {ex.Message}"); - return null; - } - } - - public async Task GetPlayerUrl(string moviePageContent) - { - try - { - // Шукаємо iframe з id="vip-player" та class="tab-content" - var match = Regex.Match(moviePageContent, @"]*id=[""']vip-player[""'][^>]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase); - if (match.Success) - { - string playerUrl = match.Groups[1].Value; - _onLog($"UaTUT found player URL: {playerUrl}"); - return playerUrl; - } - - _onLog("UaTUT: vip-player iframe not found"); - return null; - } - catch (Exception ex) - { - _onLog($"UaTUT GetPlayerUrl error: {ex.Message}"); - return null; - } - } - - public async Task GetPlayerData(string playerUrl) - { - try - { - _onLog($"UaTUT getting player data from: {playerUrl}"); - - if (IsNotAllowedHost(playerUrl)) - return null; - - var headers = new List() { new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") }; - var response = await Http.Get(playerUrl, headers: headers, proxy: _proxyManager.Get()); - - if (string.IsNullOrEmpty(response)) - return null; - - return ParsePlayerData(response); - } - catch (Exception ex) - { - _onLog($"UaTUT GetPlayerData error: {ex.Message}"); - return null; - } - } - - private static bool IsNotAllowedHost(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - return false; - - return NotAllowedHosts.Contains(uri.Host); - } - - private PlayerData ParsePlayerData(string playerHtml) - { - try - { - var playerData = new PlayerData(); - - // Для фільмів шукаємо прямий file - var fileMatch = Regex.Match(playerHtml, @"file:'([^']+)'", RegexOptions.IgnoreCase); - if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("[")) - { - playerData.File = fileMatch.Groups[1].Value; - _onLog($"UaTUT found direct file: {playerData.File}"); - - // Шукаємо poster - var posterMatch = Regex.Match(playerHtml, @"poster:[""']([^""']+)[""']", RegexOptions.IgnoreCase); - if (posterMatch.Success) - playerData.Poster = posterMatch.Groups[1].Value; - - return playerData; - } - - // Для серіалів шукаємо JSON структуру з сезонами та озвучками - var jsonMatch = Regex.Match(playerHtml, @"file:'(\[.*?\])'", RegexOptions.Singleline); - if (jsonMatch.Success) - { - string jsonData = jsonMatch.Groups[1].Value; - _onLog($"UaTUT found JSON data for series"); - - playerData.Voices = ParseVoicesJson(jsonData); - return playerData; - } - - _onLog("UaTUT: No player data found"); - return null; - } - catch (Exception ex) - { - _onLog($"UaTUT ParsePlayerData error: {ex.Message}"); - return null; - } - } - - private List ParseVoicesJson(string jsonData) - { - try - { - // Декодуємо JSON структуру озвучок - dynamic voicesData = JsonConvert.DeserializeObject(jsonData); - var voices = new List(); - - if (voicesData != null) - { - foreach (var voiceGroup in voicesData) - { - var voice = new Voice - { - Name = voiceGroup.title?.ToString(), - Seasons = new List() - }; - - if (voiceGroup.folder != null) - { - foreach (var seasonData in voiceGroup.folder) - { - var season = new Season - { - Title = seasonData.title?.ToString(), - Episodes = new List() - }; - - if (seasonData.folder != null) - { - foreach (var episodeData in seasonData.folder) - { - var episode = new Episode - { - Title = episodeData.title?.ToString(), - File = episodeData.file?.ToString(), - Id = episodeData.id?.ToString(), - Poster = episodeData.poster?.ToString(), - Subtitle = episodeData.subtitle?.ToString() - }; - season.Episodes.Add(episode); - } - } - - voice.Seasons.Add(season); - } - } - - voices.Add(voice); - } - } - - _onLog($"UaTUT parsed {voices.Count} voices"); - return voices; - } - catch (Exception ex) - { - _onLog($"UaTUT ParseVoicesJson error: {ex.Message}"); - return new List(); - } - } - } -} diff --git a/UaTUT/manifest.json b/UaTUT/manifest.json deleted file mode 100644 index 79200f2..0000000 --- a/UaTUT/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "enable": true, - "version": 2, - "initspace": "UaTUT.ModInit", - "online": "UaTUT.OnlineApi" -} - diff --git a/Uaflix/Controller.cs b/Uaflix/Controller.cs deleted file mode 100644 index 5f9d952..0000000 --- a/Uaflix/Controller.cs +++ /dev/null @@ -1,358 +0,0 @@ -using Shared.Models.Templates; -using Shared.Engine; -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; -using System.Web; -using System.Linq; -using HtmlAgilityPack; -using Shared; -using Shared.Models.Templates; -using System.Text.RegularExpressions; -using System.Text; -using Shared.Models.Online.Settings; -using Shared.Models; -using Uaflix.Models; - -namespace Uaflix.Controllers -{ - - public class Controller : BaseOnlineController - { - private static readonly HashSet NotAllowedHosts = - new HashSet( - new[] - { - "c3ZpdGFubW92aWU=", - "cG9ydGFsLXR2", - } - .Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))), - StringComparer.OrdinalIgnoreCase - ); - ProxyManager proxyManager; - - public Controller() - { - proxyManager = new ProxyManager(ModInit.UaFlix); - } - - [HttpGet] - [Route("uaflix")] - 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, string href = null, bool checksearch = false) - { - var init = await loadKit(ModInit.UaFlix); - if (await IsBadInitialization(init)) - return Forbid(); - - OnLog($"=== UAFLIX INDEX START ==="); - OnLog($"Uaflix Index: title={title}, serial={serial}, s={s}, play={play}, href={href}, checksearch={checksearch}"); - OnLog($"Uaflix Index: kinopoisk_id={kinopoisk_id}, imdb_id={imdb_id}, id={id}"); - OnLog($"Uaflix Index: year={year}, source={source}, t={t}, e={e}, rjson={rjson}"); - - var invoke = new UaflixInvoke(init, hybridCache, OnLog, proxyManager); - - // Обробка параметра checksearch - повертаємо спеціальну відповідь для валідації - if (checksearch) - { - try - { - string filmTitle = !string.IsNullOrEmpty(title) ? title : original_title; - string searchUrl = $"{init.host}/index.php?do=search&subaction=search&story={System.Web.HttpUtility.UrlEncode(filmTitle)}"; - var headers = new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) }; - - if (IsNotAllowedHost(searchUrl)) - return OnError("uaflix", proxyManager); - - var searchHtml = await Http.Get(searchUrl, headers: headers, proxy: proxyManager.Get(), timeoutSeconds: 10); - - // Швидка перевірка наявності результатів без повного парсингу - if (!string.IsNullOrEmpty(searchHtml) && - (searchHtml.Contains("sres-wrap") || searchHtml.Contains("sres-item") || searchHtml.Contains("search-results"))) - { - // Якщо знайдено контент, повертаємо "data-json=" для валідації - OnLog("checksearch: Content found, returning validation response"); - OnLog("=== RETURN: checksearch validation (data-json=) ==="); - return Content("data-json=", "text/plain; charset=utf-8"); - } - else - { - // Якщо нічого не знайдено, повертаємо OnError - OnLog("checksearch: No content found"); - OnLog("=== RETURN: checksearch OnError ==="); - return OnError("uaflix", proxyManager); - } - } - catch (Exception ex) - { - OnLog($"checksearch error: {ex.Message}"); - OnLog("=== RETURN: checksearch exception OnError ==="); - return OnError("uaflix", proxyManager); - } - } - - if (play) - { - // Визначаємо URL для парсингу - або з параметра t, або з episode_url - string urlToParse = !string.IsNullOrEmpty(t) ? t : Request.Query["episode_url"]; - - var playResult = await invoke.ParseEpisode(urlToParse); - if (playResult.streams != null && playResult.streams.Count > 0) - { - OnLog("=== RETURN: play redirect ==="); - return Redirect(HostStreamProxy(init, accsArgs(playResult.streams.First().link))); - } - - OnLog("=== RETURN: play no streams ==="); - return Content("Uaflix", "text/html; charset=utf-8"); - } - - // Якщо є episode_url але немає play=true, це виклик для отримання інформації про стрім (для method: 'call') - string episodeUrl = Request.Query["episode_url"]; - if (!string.IsNullOrEmpty(episodeUrl)) - { - var playResult = await invoke.ParseEpisode(episodeUrl); - if (playResult.streams != null && playResult.streams.Count > 0) - { - // Повертаємо JSON з інформацією про стрім для методу 'play' - string streamUrl = HostStreamProxy(init, accsArgs(playResult.streams.First().link)); - string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? original_title}\"}}"; - OnLog($"=== RETURN: call method JSON for episode_url ==="); - return Content(jsonResult, "application/json; charset=utf-8"); - } - - OnLog("=== RETURN: call method no streams ==="); - return Content("Uaflix", "text/html; charset=utf-8"); - } - - string filmUrl = href; - - if (string.IsNullOrEmpty(filmUrl)) - { - var searchResults = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, title); - if (searchResults == null || searchResults.Count == 0) - { - OnLog("No search results found"); - OnLog("=== RETURN: no search results OnError ==="); - return OnError("uaflix", proxyManager); - } - - // Для фільмів і серіалів показуємо вибір тільки якщо більше одного результату - if (searchResults.Count > 1) - { - var similar_tpl = new SimilarTpl(searchResults.Count); - foreach (var res in searchResults) - { - 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={serial}&href={HttpUtility.UrlEncode(res.Url)}"; - similar_tpl.Append(res.Title, res.Year.ToString(), string.Empty, link, res.PosterUrl); - } - OnLog($"=== RETURN: similar items ({searchResults.Count}) ==="); - return rjson ? Content(similar_tpl.ToJson(), "application/json; charset=utf-8") : Content(similar_tpl.ToHtml(), "text/html; charset=utf-8"); - } - - filmUrl = searchResults[0].Url; - OnLog($"Auto-selected first search result: {filmUrl}"); - } - - if (serial == 1) - { - // Агрегуємо всі озвучки з усіх плеєрів - var structure = await invoke.AggregateSerialStructure(filmUrl); - if (structure == null || !structure.Voices.Any()) - { - OnLog("No voices found in aggregated structure"); - OnLog("=== RETURN: no voices OnError ==="); - return OnError("uaflix", proxyManager); - } - - OnLog($"Structure aggregated successfully: {structure.Voices.Count} voices, URL: {filmUrl}"); - foreach (var voice in structure.Voices) - { - OnLog($"Voice: {voice.Key}, Type: {voice.Value.PlayerType}, Seasons: {voice.Value.Seasons.Count}"); - foreach (var season in voice.Value.Seasons) - { - OnLog($" Season {season.Key}: {season.Value.Count} episodes"); - } - } - - // s == -1: Вибір сезону - if (s == -1) - { - var allSeasons = structure.Voices - .SelectMany(v => v.Value.Seasons.Keys) - .Distinct() - .OrderBy(sn => sn) - .ToList(); - - OnLog($"Found {allSeasons.Count} seasons in structure: {string.Join(", ", allSeasons)}"); - - // Перевіряємо чи сезони містять валідні епізоди з файлами - var seasonsWithValidEpisodes = allSeasons.Where(season => - structure.Voices.Values.Any(v => - v.Seasons.ContainsKey(season) && - v.Seasons[season].Any(ep => !string.IsNullOrEmpty(ep.File)) - ) - ).ToList(); - - OnLog($"Seasons with valid episodes: {seasonsWithValidEpisodes.Count}"); - foreach (var season in allSeasons) - { - var episodesInSeason = structure.Voices.Values - .Where(v => v.Seasons.ContainsKey(season)) - .SelectMany(v => v.Seasons[season]) - .Where(ep => !string.IsNullOrEmpty(ep.File)) - .ToList(); - OnLog($"Season {season}: {episodesInSeason.Count} valid episodes"); - } - - if (!seasonsWithValidEpisodes.Any()) - { - OnLog("No seasons with valid episodes found in structure"); - OnLog("=== RETURN: no valid seasons OnError ==="); - return OnError("uaflix", proxyManager); - } - - var season_tpl = new SeasonTpl(seasonsWithValidEpisodes.Count); - 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)}"; - season_tpl.Append($"{season}", link, season.ToString()); - OnLog($"Added season {season} to template"); - } - - OnLog($"Returning season template with {seasonsWithValidEpisodes.Count} seasons"); - - var htmlContent = rjson ? season_tpl.ToJson() : season_tpl.ToHtml(); - OnLog($"Season template response length: {htmlContent.Length}"); - OnLog($"Season template HTML (first 300): {htmlContent.Substring(0, Math.Min(300, htmlContent.Length))}"); - OnLog($"=== RETURN: season template ({seasonsWithValidEpisodes.Count} seasons) ==="); - - return Content(htmlContent, rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8"); - } - // s >= 0: Показуємо озвучки + епізоди - else if (s >= 0) - { - var voicesForSeason = structure.Voices - .Where(v => v.Value.Seasons.ContainsKey(s)) - .Select(v => new { DisplayName = v.Key, Info = v.Value }) - .ToList(); - - if (!voicesForSeason.Any()) - { - OnLog($"No voices found for season {s}"); - OnLog("=== RETURN: no voices for season OnError ==="); - return OnError("uaflix", proxyManager); - } - - // Автоматично вибираємо першу озвучку якщо не вказана - if (string.IsNullOrEmpty(t)) - { - t = voicesForSeason[0].DisplayName; - OnLog($"Auto-selected first voice: {t}"); - } - - // Створюємо VoiceTpl з усіма озвучками - var voice_tpl = new VoiceTpl(); - 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 isActive = voice.DisplayName == t; - voice_tpl.Append(voice.DisplayName, isActive, voiceLink); - } - OnLog($"Created VoiceTpl with {voicesForSeason.Count} voices, active: {t}"); - - // Відображення епізодів для вибраної озвучки - if (!structure.Voices.ContainsKey(t)) - { - OnLog($"Voice '{t}' not found in structure"); - OnLog("=== RETURN: voice not found OnError ==="); - return OnError("uaflix", proxyManager); - } - - if (!structure.Voices[t].Seasons.ContainsKey(s)) - { - OnLog($"Season {s} not found for voice '{t}'"); - OnLog("=== RETURN: season not found for voice OnError ==="); - return OnError("uaflix", proxyManager); - } - - var episodes = structure.Voices[t].Seasons[s]; - var episode_tpl = new EpisodeTpl(); - - foreach (var ep in episodes) - { - // Для zetvideo-vod повертаємо URL епізоду з методом call - // Для ashdi/zetvideo-serial повертаємо готове посилання з play - var voice = structure.Voices[t]; - - if (voice.PlayerType == "zetvideo-vod" || voice.PlayerType == "ashdi-vod") - { - // Для zetvideo-vod та ashdi-vod використовуємо URL епізоду для виклику - // Потрібно передати URL епізоду в інший параметр, щоб не плутати з play=true - string callUrl = $"{host}/uaflix?episode_url={HttpUtility.UrlEncode(ep.File)}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&s={s}&e={ep.Number}"; - episode_tpl.Append( - name: ep.Title, - title: title, - s: s.ToString(), - e: ep.Number.ToString(), - link: accsArgs(callUrl), - method: "call", - streamlink: accsArgs($"{callUrl}&play=true") - ); - } - else - { - // Для багатосерійних плеєрів (ashdi-serial, zetvideo-serial) - пряме відтворення - string playUrl = HostStreamProxy(init, accsArgs(ep.File)); - episode_tpl.Append( - name: ep.Title, - title: title, - s: s.ToString(), - e: ep.Number.ToString(), - link: playUrl - ); - } - } - - OnLog($"Created EpisodeTpl with {episodes.Count} episodes"); - - // Повертаємо VoiceTpl + EpisodeTpl разом - if (rjson) - { - OnLog($"=== RETURN: episode template with voices JSON ({episodes.Count} episodes) ==="); - return Content(episode_tpl.ToJson(voice_tpl), "application/json; charset=utf-8"); - } - else - { - OnLog($"=== RETURN: voice + episode template HTML ({episodes.Count} episodes) ==="); - return Content(voice_tpl.ToHtml() + episode_tpl.ToHtml(), "text/html; charset=utf-8"); - } - } - - // Fallback: якщо жоден з умов не виконався - OnLog($"Fallback: s={s}, t={t}"); - OnLog("=== RETURN: fallback OnError ==="); - return OnError("uaflix", proxyManager); - } - else // Фільм - { - string link = $"{host}/uaflix?t={HttpUtility.UrlEncode(filmUrl)}&play=true"; - var tpl = new MovieTpl(title, original_title, 1); - tpl.Append(title, accsArgs(link), method: "play"); - OnLog("=== RETURN: movie template ==="); - return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8"); - } - } - - private static bool IsNotAllowedHost(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - return false; - - return NotAllowedHosts.Contains(uri.Host); - } - } -} diff --git a/Uaflix/ModInit.cs b/Uaflix/ModInit.cs deleted file mode 100644 index 252a56c..0000000 --- a/Uaflix/ModInit.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Newtonsoft.Json; -using Shared; -using Shared.Engine; -using Newtonsoft.Json.Linq; -using Shared.Models.Online.Settings; -using Shared.Models.Module; - -namespace Uaflix -{ - public class ModInit - { - public static OnlinesSettings UaFlix; - - /// - /// модуль загружен - /// - public static void loaded(InitspaceModel initspace) - { - UaFlix = new OnlinesSettings("Uaflix", "https://uafix.net", streamproxy: false, useproxy: false) - { - displayname = "UaFlix", - group = 0, - group_hide = false, - globalnameproxy = null, - displayindex = 0, - proxy = new Shared.Models.Base.ProxySettings() - { - useAuth = true, - username = "a", - password = "a", - list = new string[] { "socks5://IP:PORT" } - }, - // Note: OnlinesSettings не має властивості additional, використовуємо інший підхід - }; - - UaFlix = ModuleInvoke.Conf("Uaflix", UaFlix).ToObject(); - - // Виводити "уточнити пошук" - AppInit.conf.online.with_search.Add("uaflix"); - } - } -} diff --git a/Uaflix/Models/EpisodeLinkInfo.cs b/Uaflix/Models/EpisodeLinkInfo.cs deleted file mode 100644 index 2180106..0000000 --- a/Uaflix/Models/EpisodeLinkInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Uaflix.Models -{ - public class EpisodeLinkInfo - { - public string url { get; set; } - public string title { get; set; } - public int season { get; set; } - public int episode { get; set; } - - // Нові поля для підтримки змішаних плеєрів - public string playerType { get; set; } // "ashdi-serial", "zetvideo-serial", "zetvideo-vod", "ashdi-vod" - public string iframeUrl { get; set; } // URL iframe для цього епізоду - } -} \ No newline at end of file diff --git a/Uaflix/Models/FilmInfo.cs b/Uaflix/Models/FilmInfo.cs deleted file mode 100644 index 2b8d937..0000000 --- a/Uaflix/Models/FilmInfo.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Uaflix.Models -{ - /// - /// Модель для зберігання інформації про фільм - /// - public class FilmInfo - { - /// - /// URL сторінки фільму - /// - public string Url { get; set; } - - /// - /// Назва фільму - /// - public string Title { get; set; } - - /// - /// Рік випуску - /// - public int Year { get; set; } - - /// - /// Опис фільму - /// - public string Description { get; set; } - - /// - /// Постер фільму - /// - public string PosterUrl { get; set; } - - /// - /// Список акторів - /// - public List Actors { get; set; } = new List(); - - /// - /// Режисер - /// - public string Director { get; set; } - - /// - /// Тривалість у секундах - /// - public int Duration { get; set; } - } -} \ No newline at end of file diff --git a/Uaflix/Models/PaginationInfo.cs b/Uaflix/Models/PaginationInfo.cs deleted file mode 100644 index 495afe9..0000000 --- a/Uaflix/Models/PaginationInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Uaflix.Models -{ - public class PaginationInfo - { - // Словник сезонів, де ключ - номер сезону, значення - кількість сторінок - public Dictionary Seasons { get; set; } = new Dictionary(); - - // Загальна кількість сторінок (якщо потрібно) - public int TotalPages { get; set; } - - // URL сторінки серіалу (базовий URL для пагінації) - public string SerialUrl { get; set; } - - public List Episodes { get; set; } = new List(); - } -} \ No newline at end of file diff --git a/Uaflix/Models/PlayResult.cs b/Uaflix/Models/PlayResult.cs deleted file mode 100644 index 2275af7..0000000 --- a/Uaflix/Models/PlayResult.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Shared.Models.Templates; - -namespace Uaflix.Models -{ - public class PlayResult - { - public string ashdi_url { get; set; } - public List<(string link, string quality)> streams { get; set; } - public SubtitleTpl? subtitles { get; set; } - } -} \ No newline at end of file diff --git a/Uaflix/Models/SearchResult.cs b/Uaflix/Models/SearchResult.cs deleted file mode 100644 index 832a22f..0000000 --- a/Uaflix/Models/SearchResult.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Uaflix.Models -{ - public class SearchResult - { - public string Title { get; set; } - public string Url { get; set; } - public int Year { get; set; } - public string PosterUrl { get; set; } - } -} \ No newline at end of file diff --git a/Uaflix/Models/SerialAggregatedStructure.cs b/Uaflix/Models/SerialAggregatedStructure.cs deleted file mode 100644 index d0deede..0000000 --- a/Uaflix/Models/SerialAggregatedStructure.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; - -namespace Uaflix.Models -{ - /// - /// Агрегована структура серіалу з озвучками з усіх джерел (ashdi, zetvideo-serial, zetvideo-vod, ashdi-vod) - /// - public class SerialAggregatedStructure - { - /// - /// URL головної сторінки серіалу - /// - public string SerialUrl { get; set; } - - /// - /// Словник озвучок: ключ - displayName озвучки (наприклад, "[Ashdi] DniproFilm"), значення - VoiceInfo - /// - public Dictionary Voices { get; set; } - - /// - /// Список всіх епізодів серіалу (використовується для zetvideo-vod) - /// - public List AllEpisodes { get; set; } - - public SerialAggregatedStructure() - { - Voices = new Dictionary(); - AllEpisodes = new List(); - } - } -} \ No newline at end of file diff --git a/Uaflix/Models/VoiceInfo.cs b/Uaflix/Models/VoiceInfo.cs deleted file mode 100644 index 2e6a474..0000000 --- a/Uaflix/Models/VoiceInfo.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Generic; - -namespace Uaflix.Models -{ - /// - /// Модель для зберігання інформації про озвучку серіалу - /// - public class VoiceInfo - { - /// - /// Назва озвучки без префіксу (наприклад, "DniproFilm") - /// - public string Name { get; set; } - - /// - /// Тип плеєра: "ashdi-serial", "zetvideo-serial", "zetvideo-vod", "ashdi-vod" - /// - public string PlayerType { get; set; } - - /// - /// Назва для відображення з префіксом плеєра (наприклад, "[Ashdi] DniproFilm") - /// - public string DisplayName { get; set; } - - /// - /// Словник сезонів: ключ - номер сезону, значення - список епізодів - /// - public Dictionary> Seasons { get; set; } - - public VoiceInfo() - { - Seasons = new Dictionary>(); - } - } - - /// - /// Модель для зберігання інформації про окремий епізод - /// - public class EpisodeInfo - { - /// - /// Номер епізоду - /// - public int Number { get; set; } - - /// - /// Назва епізоду - /// - public string Title { get; set; } - - /// - /// Пряме посилання на відео файл (m3u8) - /// - public string File { get; set; } - - /// - /// ID епізоду у плеєрі - /// - public string Id { get; set; } - - /// - /// URL постера епізоду - /// - public string Poster { get; set; } - - /// - /// Субтитри у форматі Playerjs - /// - public string Subtitle { get; set; } - } -} \ No newline at end of file diff --git a/Uaflix/OnlineApi.cs b/Uaflix/OnlineApi.cs deleted file mode 100644 index 8f59a73..0000000 --- a/Uaflix/OnlineApi.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Shared.Models.Base; -using System.Collections.Generic; - -namespace Uaflix -{ - 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.UaFlix; - if (init.enable && !init.rip) - { - string url = init.overridehost; - if (string.IsNullOrEmpty(url)) - url = $"{host}/uaflix"; - - online.Add((init.displayname, url, "uaflix", init.displayindex)); - } - - return online; - } - } -} diff --git a/Uaflix/Uaflix.csproj b/Uaflix/Uaflix.csproj deleted file mode 100644 index c26a806..0000000 --- a/Uaflix/Uaflix.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net9.0 - library - true - - - - - ..\..\Shared.dll - - - - \ No newline at end of file diff --git a/Uaflix/UaflixInvoke.cs b/Uaflix/UaflixInvoke.cs deleted file mode 100644 index 224f245..0000000 --- a/Uaflix/UaflixInvoke.cs +++ /dev/null @@ -1,1082 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Shared; -using Shared.Models.Online.Settings; -using Shared.Models; -using System.Text.RegularExpressions; -using HtmlAgilityPack; -using Uaflix.Controllers; -using Shared.Engine; -using Uaflix.Models; -using System.Linq; -using Shared.Models.Templates; -using System.Net; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Text; - -namespace Uaflix -{ - public class UaflixInvoke - { - private static readonly HashSet NotAllowedHosts = - new HashSet( - new[] - { - "c3ZpdGFubW92aWU=", - "cG9ydGFsLXR2", - } - .Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))), - StringComparer.OrdinalIgnoreCase - ); - private OnlinesSettings _init; - private HybridCache _hybridCache; - private Action _onLog; - private ProxyManager _proxyManager; - - public UaflixInvoke(OnlinesSettings init, HybridCache hybridCache, Action onLog, ProxyManager proxyManager) - { - _init = init; - _hybridCache = hybridCache; - _onLog = onLog; - _proxyManager = proxyManager; - } - - #region Методи для визначення та парсингу різних типів плеєрів - - /// - /// Визначити тип плеєра з URL iframe - /// - private string DeterminePlayerType(string iframeUrl) - { - if (string.IsNullOrEmpty(iframeUrl)) - return null; - - // Перевіряємо на підтримувані типи плеєрів - if (iframeUrl.Contains("ashdi.vip/serial/")) - return "ashdi-serial"; - else if (iframeUrl.Contains("ashdi.vip/vod/")) - return "ashdi-vod"; - else if (iframeUrl.Contains("zetvideo.net/serial/")) - return "zetvideo-serial"; - else if (iframeUrl.Contains("zetvideo.net/vod/")) - return "zetvideo-vod"; - - // Перевіряємо на небажані типи плеєрів (трейлери, реклама тощо) - if (iframeUrl.Contains("youtube.com/embed/") || - iframeUrl.Contains("youtu.be/") || - iframeUrl.Contains("vimeo.com/") || - iframeUrl.Contains("dailymotion.com/")) - return "trailer"; // Ігноруємо відеохостинги з трейлерами - - return null; - } - - /// - /// Парсинг багатосерійного плеєра (ashdi-serial або zetvideo-serial) - /// - private async Task> ParseMultiEpisodePlayer(string iframeUrl, string playerType) - { - string referer = "https://uafix.net/"; - - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", referer) - }; - - try - { - // Для ashdi видаляємо параметри season та episode для отримання всіх озвучок - string requestUrl = iframeUrl; - if (playerType == "ashdi-serial" && iframeUrl.Contains("ashdi.vip/serial/")) - { - // Витягуємо базовий URL без параметрів - var baseUrlMatch = Regex.Match(iframeUrl, @"(https://ashdi\.vip/serial/\d+)"); - if (baseUrlMatch.Success) - { - requestUrl = baseUrlMatch.Groups[1].Value; - _onLog($"ParseMultiEpisodePlayer: Using base ashdi URL without parameters: {requestUrl}"); - } - } - - if (IsNotAllowedHost(requestUrl)) - return null; - - string html = await Http.Get(requestUrl, headers: headers, proxy: _proxyManager.Get()); - - // Знайти JSON у new Playerjs({file:'...'}) - var match = Regex.Match(html, @"file:'(\[.+?\])'", RegexOptions.Singleline); - if (!match.Success) - { - _onLog($"ParseMultiEpisodePlayer: JSON not found in iframe {iframeUrl}"); - return new List(); - } - - string jsonStr = match.Groups[1].Value - .Replace("\\'", "'") - .Replace("\\\"", "\""); - - var voicesArray = JsonConvert.DeserializeObject>(jsonStr); - var voices = new List(); - - string playerPrefix = playerType == "ashdi-serial" ? "Ashdi" : "Zetvideo"; - - // Для формування унікальних назв озвучок - var voiceCounts = new Dictionary(); - - foreach (var voiceObj in voicesArray) - { - string voiceName = voiceObj["title"]?.ToString().Trim(); - if (string.IsNullOrEmpty(voiceName)) - continue; - - // Перевіряємо, чи вже існує така назва озвучки - if (voiceCounts.ContainsKey(voiceName)) - { - voiceCounts[voiceName]++; - // Якщо є дублікат, додаємо номер - voiceName = $"{voiceName} {voiceCounts[voiceName]}"; - } - else - { - // Ініціалізуємо лічильник для нової озвучки - voiceCounts[voiceObj["title"]?.ToString().Trim()] = 1; - } - - var voiceInfo = new VoiceInfo - { - Name = voiceObj["title"]?.ToString().Trim(), // Зберігаємо оригінальну назву для внутрішнього використання - PlayerType = playerType, - DisplayName = voiceName, // Відображаємо унікальну назву - Seasons = new Dictionary>() - }; - - var seasons = voiceObj["folder"] as JArray; - if (seasons != null) - { - foreach (var seasonObj in seasons) - { - string seasonTitle = seasonObj["title"]?.ToString(); - var seasonMatch = Regex.Match(seasonTitle, @"Сезон\s+(\d+)", RegexOptions.IgnoreCase); - - if (!seasonMatch.Success) - continue; - - int seasonNumber = int.Parse(seasonMatch.Groups[1].Value); - var episodes = new List(); - var episodesArray = seasonObj["folder"] as JArray; - - if (episodesArray != null) - { - int episodeNum = 1; - foreach (var epObj in episodesArray) - { - episodes.Add(new EpisodeInfo - { - Number = episodeNum++, - Title = epObj["title"]?.ToString(), - File = epObj["file"]?.ToString(), - Id = epObj["id"]?.ToString(), - Poster = epObj["poster"]?.ToString(), - Subtitle = epObj["subtitle"]?.ToString() - }); - } - } - - voiceInfo.Seasons[seasonNumber] = episodes; - } - } - - voices.Add(voiceInfo); - } - - _onLog($"ParseMultiEpisodePlayer: Found {voices.Count} voices in {playerType}"); - return voices; - } - catch (Exception ex) - { - _onLog($"ParseMultiEpisodePlayer error: {ex.Message}"); - return new List(); - } - } - - /// - /// Парсинг одного епізоду з zetvideo-vod - /// - private async Task<(string file, string voiceName)> ParseSingleEpisodePlayer(string iframeUrl) - { - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", "https://uafix.net/") - }; - - try - { - if (IsNotAllowedHost(iframeUrl)) - return (null, null); - - string html = await Http.Get(iframeUrl, headers: headers, proxy: _proxyManager.Get()); - - // Знайти file:"url" - var match = Regex.Match(html, @"file:\s*""([^""]+\.m3u8)"""); - if (!match.Success) - return (null, null); - - string fileUrl = match.Groups[1].Value; - - // Визначити озвучку з URL - string voiceName = ExtractVoiceFromUrl(fileUrl); - - return (fileUrl, voiceName); - } - catch (Exception ex) - { - _onLog($"ParseSingleEpisodePlayer error: {ex.Message}"); - return (null, null); - } - } - - /// - /// Парсинг одного епізоду з ashdi-vod (новий метод для обробки окремих епізодів з ashdi.vip/vod/) - /// - private async Task<(string file, string voiceName)> ParseAshdiVodEpisode(string iframeUrl) - { - var headers = new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", "https://uafix.net/") - }; - - try - { - if (IsNotAllowedHost(iframeUrl)) - return (null, null); - - string html = await Http.Get(iframeUrl, headers: headers, proxy: _proxyManager.Get()); - - // Шукаємо Playerjs конфігурацію з file параметром - var match = Regex.Match(html, @"file:\s*'?([^'""\s,}]+\.m3u8)'?"); - if (!match.Success) - { - // Якщо не знайдено, шукаємо в іншому форматі - match = Regex.Match(html, @"file['""]?\s*:\s*['""]([^'""}]+\.m3u8)['""]"); - } - - if (!match.Success) - return (null, null); - - string fileUrl = match.Groups[1].Value; - - // Визначити озвучку з URL - string voiceName = ExtractVoiceFromUrl(fileUrl); - - return (fileUrl, voiceName); - } - catch (Exception ex) - { - _onLog($"ParseAshdiVodEpisode error: {ex.Message}"); - return (null, null); - } - } - - /// - /// Витягнути назву озвучки з URL файлу - /// - private string ExtractVoiceFromUrl(string fileUrl) - { - if (string.IsNullOrEmpty(fileUrl)) - return "Невідомо"; - - if (fileUrl.Contains("uaflix")) - return "Uaflix"; - else if (fileUrl.Contains("dniprofilm")) - return "DniproFilm"; - else if (fileUrl.Contains("newstudio")) - return "NewStudio"; - - return "Невідомо"; - } - - #endregion - - #region Агрегація структури серіалу з усіх джерел - - /// - /// Агрегує озвучки з усіх епізодів серіалу (ashdi, zetvideo-serial, zetvideo-vod) - /// - public async Task AggregateSerialStructure(string serialUrl) - { - string memKey = $"UaFlix:aggregated:{serialUrl}"; - if (_hybridCache.TryGetValue(memKey, out SerialAggregatedStructure cached)) - { - _onLog($"AggregateSerialStructure: Using cached structure for {serialUrl}"); - return cached; - } - - try - { - // Edge Case 1: Перевірка валідності URL - if (string.IsNullOrEmpty(serialUrl) || !Uri.IsWellFormedUriString(serialUrl, UriKind.Absolute)) - { - _onLog($"AggregateSerialStructure: Invalid URL: {serialUrl}"); - return null; - } - - // Отримати список всіх епізодів - var paginationInfo = await GetPaginationInfo(serialUrl); - if (paginationInfo?.Episodes == null || !paginationInfo.Episodes.Any()) - { - _onLog($"AggregateSerialStructure: No episodes found for {serialUrl}"); - return null; - } - - var structure = new SerialAggregatedStructure - { - SerialUrl = serialUrl, - Voices = new Dictionary(), - AllEpisodes = paginationInfo.Episodes - }; - - // Групуємо епізоди по сезонах - var episodesBySeason = paginationInfo.Episodes - .GroupBy(e => e.season) - .ToDictionary(g => g.Key, g => g.ToList()); - - _onLog($"AggregateSerialStructure: Processing {episodesBySeason.Count} seasons"); - - // Для кожного сезону беремо перший епізод та визначаємо тип плеєра - foreach (var seasonGroup in episodesBySeason) - { - int season = seasonGroup.Key; - var firstEpisode = seasonGroup.Value.First(); - - _onLog($"AggregateSerialStructure: Processing season {season}, first episode: {firstEpisode.url}"); - - // Отримати HTML епізоду та знайти iframe - var headers = new List() { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", _init.host) - }; - - if (IsNotAllowedHost(firstEpisode.url)) - continue; - - string html = await Http.Get(firstEpisode.url, headers: headers, proxy: _proxyManager.Get()); - - var doc = new HtmlDocument(); - doc.LoadHtml(html); - var iframe = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'video-box')]//iframe"); - - if (iframe == null) - { - _onLog($"AggregateSerialStructure: No iframe found for season {season}"); - continue; - } - - string iframeUrl = iframe.GetAttributeValue("src", "").Replace("&", "&"); - if (iframeUrl.StartsWith("//")) - iframeUrl = "https:" + iframeUrl; - - // Edge Case 2: Перевірка валідності iframe URL - if (string.IsNullOrEmpty(iframeUrl)) - { - _onLog($"AggregateSerialStructure: Empty iframe URL for season {season}"); - continue; - } - - string playerType = DeterminePlayerType(iframeUrl); - _onLog($"AggregateSerialStructure: Season {season} has playerType: {playerType}"); - - // Edge Case 3: Невідомий тип плеєра або YouTube трейлер - if (string.IsNullOrEmpty(playerType)) - { - _onLog($"AggregateSerialStructure: Unknown player type for iframe {iframeUrl} in season {season}"); - continue; - } - - // Ігноруємо трейлери та небажані відеохостинги - if (playerType == "trailer") - { - _onLog($"AggregateSerialStructure: Ignoring trailer/video host for iframe {iframeUrl} in season {season}"); - continue; - } - - if (playerType == "ashdi-serial" || playerType == "zetvideo-serial") - { - // Парсимо багатосерійний плеєр - var voices = await ParseMultiEpisodePlayer(iframeUrl, playerType); - - // Edge Case 4: Порожній результат парсингу - if (voices == null || !voices.Any()) - { - _onLog($"AggregateSerialStructure: No voices found in {playerType} for season {season}"); - continue; - } - - foreach (var voice in voices) - { - // Edge Case 5: Перевірка валідності озвучки - if (voice == null || string.IsNullOrEmpty(voice.DisplayName)) - { - _onLog($"AggregateSerialStructure: Invalid voice data in season {season}"); - continue; - } - - // Додаємо або об'єднуємо з існуючою озвучкою - if (!structure.Voices.ContainsKey(voice.DisplayName)) - { - structure.Voices[voice.DisplayName] = voice; - } - else - { - // Об'єднуємо сезони - foreach (var seasonEpisodes in voice.Seasons) - { - structure.Voices[voice.DisplayName].Seasons[seasonEpisodes.Key] = seasonEpisodes.Value; - } - } - } - } - else if (playerType == "zetvideo-vod") - { - _onLog($"AggregateSerialStructure: Processing zetvideo-vod for season {season} with {seasonGroup.Value.Count} episodes"); - - // Для zetvideo-vod створюємо озвучку з реальними епізодами - string displayName = "Uaflix #2"; - - if (!structure.Voices.ContainsKey(displayName)) - { - structure.Voices[displayName] = new VoiceInfo - { - Name = "Uaflix", - PlayerType = "zetvideo-vod", - DisplayName = displayName, - Seasons = new Dictionary>() - }; - } - - // Створюємо епізоди для цього сезону з посиланнями на сторінки епізодів - var episodes = new List(); - foreach (var episodeInfo in seasonGroup.Value) - { - episodes.Add(new EpisodeInfo - { - Number = episodeInfo.episode, - Title = episodeInfo.title, - File = episodeInfo.url, // URL сторінки епізоду для використання в call - Id = episodeInfo.url, - Poster = null, - Subtitle = null - }); - } - - structure.Voices[displayName].Seasons[season] = episodes; - - _onLog($"AggregateSerialStructure: Created voice with {episodes.Count} episodes for season {season} in zetvideo-vod"); - } - else if (playerType == "ashdi-vod") - { - _onLog($"AggregateSerialStructure: Processing ashdi-vod for season {season} with {seasonGroup.Value.Count} episodes"); - - // Для ashdi-vod створюємо озвучку з реальними епізодами - string displayName = "Uaflix #3"; - - if (!structure.Voices.ContainsKey(displayName)) - { - structure.Voices[displayName] = new VoiceInfo - { - Name = "Uaflix", - PlayerType = "ashdi-vod", - DisplayName = displayName, - Seasons = new Dictionary>() - }; - } - - // Створюємо епізоди для цього сезону з посиланнями на сторінки епізодів - var episodes = new List(); - foreach (var episodeInfo in seasonGroup.Value) - { - episodes.Add(new EpisodeInfo - { - Number = episodeInfo.episode, - Title = episodeInfo.title, - File = episodeInfo.url, // URL сторінки епізоду для використання в call - Id = episodeInfo.url, - Poster = null, - Subtitle = null - }); - } - - structure.Voices[displayName].Seasons[season] = episodes; - - _onLog($"AggregateSerialStructure: Created voice with {episodes.Count} episodes for season {season} in ashdi-vod"); - } - } - - // Edge Case 8: Перевірка наявності озвучок після агрегації - if (!structure.Voices.Any()) - { - _onLog($"AggregateSerialStructure: No voices found after aggregation for {serialUrl}"); - return null; - } - - NormalizeUaflixVoiceNames(structure); - - // Edge Case 9: Перевірка наявності епізодів у озвучках - bool hasEpisodes = structure.Voices.Values.Any(v => v.Seasons.Values.Any(s => s.Any())); - if (!hasEpisodes) - { - _onLog($"AggregateSerialStructure: No episodes found in any voice for {serialUrl}"); - _onLog($"AggregateSerialStructure: Voices count: {structure.Voices.Count}"); - foreach (var voice in structure.Voices) - { - _onLog($" Voice {voice.Key}: {voice.Value.Seasons.Sum(s => s.Value.Count)} total episodes"); - } - return null; - } - - _hybridCache.Set(memKey, structure, cacheTime(40)); - _onLog($"AggregateSerialStructure: Cached structure with {structure.Voices.Count} total voices"); - - // Детальне логування структури для діагностики - foreach (var voice in structure.Voices) - { - _onLog($" Voice: {voice.Key} ({voice.Value.PlayerType}) - Seasons: {voice.Value.Seasons.Count}"); - foreach (var season in voice.Value.Seasons) - { - _onLog($" Season {season.Key}: {season.Value.Count} episodes"); - foreach (var episode in season.Value.Take(3)) // Показуємо тільки перші 3 епізоди - { - _onLog($" Episode {episode.Number}: {episode.Title} - {episode.File}"); - } - if (season.Value.Count > 3) - _onLog($" ... and {season.Value.Count - 3} more episodes"); - } - } - - return structure; - } - catch (Exception ex) - { - _onLog($"AggregateSerialStructure error: {ex.Message}"); - return null; - } - } - - #endregion - - public async Task> Search(string imdb_id, long kinopoisk_id, string title, string original_title, int year, string search_query) - { - string memKey = $"UaFlix:search:{kinopoisk_id}:{imdb_id}:{search_query}"; - if (_hybridCache.TryGetValue(memKey, out List res)) - return res; - - try - { - string filmTitle = !string.IsNullOrEmpty(original_title) ? original_title : (!string.IsNullOrEmpty(title) ? title : search_query); - string searchUrl = $"{_init.host}/index.php?do=search&subaction=search&story={System.Web.HttpUtility.UrlEncode(filmTitle)}"; - var headers = new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }; - - if (IsNotAllowedHost(searchUrl)) - return null; - - var searchHtml = await Http.Get(searchUrl, headers: headers, proxy: _proxyManager.Get()); - var doc = new HtmlDocument(); - doc.LoadHtml(searchHtml); - - // Спробуємо різні селектори для пошуку результатів - var filmNodes = doc.DocumentNode.SelectNodes("//a[contains(@class, 'sres-wrap')]") ?? - doc.DocumentNode.SelectNodes("//div[contains(@class, 'sres-item')]//a") ?? - doc.DocumentNode.SelectNodes("//div[contains(@class, 'search-result')]//a") ?? - doc.DocumentNode.SelectNodes("//a[contains(@href, '/serials/') or contains(@href, '/films/')]"); - - if (filmNodes == null || filmNodes.Count == 0) - { - _onLog($"Search: No search results found with any selector for query: {filmTitle}"); - return null; - } - - res = new List(); - foreach (var filmNode in filmNodes) - { - try - { - var h2Node = filmNode.SelectSingleNode(".//h2") ?? filmNode.SelectSingleNode(".//h3"); - if (h2Node == null) continue; - - string filmUrl = filmNode.GetAttributeValue("href", ""); - if (string.IsNullOrEmpty(filmUrl)) continue; - - if (!filmUrl.StartsWith("http")) - filmUrl = _init.host + filmUrl; - - // Спробуємо різні способи отримати рік - int filmYear = 0; - var descNode = filmNode.SelectSingleNode(".//div[contains(@class, 'sres-desc')]") ?? - filmNode.SelectSingleNode(".//span[contains(@class, 'year')]") ?? - filmNode.SelectSingleNode(".//*[contains(text(), '20')]"); - - if (descNode != null) - { - string yearText = descNode.InnerText ?? ""; - var yearMatch = Regex.Match(yearText, @"(?:19|20)\d{2}"); - if (yearMatch.Success) - int.TryParse(yearMatch.Value, out filmYear); - } - - // Спробуємо різні селектори для постера - var posterNode = filmNode.SelectSingleNode(".//img[@src]") ?? - filmNode.SelectSingleNode(".//img[@data-src]") ?? - filmNode.SelectSingleNode(".//div[contains(@class, 'poster')]//img"); - - string posterUrl = posterNode?.GetAttributeValue("src", "") ?? posterNode?.GetAttributeValue("data-src", ""); - if (!string.IsNullOrEmpty(posterUrl) && !posterUrl.StartsWith("http")) - posterUrl = _init.host + posterUrl; - - res.Add(new SearchResult - { - Title = h2Node.InnerText.Trim(), - Url = filmUrl, - Year = filmYear, - PosterUrl = posterUrl - }); - - _onLog($"Search: Found result - {h2Node.InnerText.Trim()}, URL: {filmUrl}"); - } - catch (Exception ex) - { - _onLog($"Search: Error processing film node: {ex.Message}"); - continue; - } - } - - if (res.Count > 0) - { - _hybridCache.Set(memKey, res, cacheTime(20)); - return res; - } - } - catch (Exception ex) - { - _onLog($"UaFlix search error: {ex.Message}"); - } - return null; - } - - public async Task GetFilmInfo(string filmUrl) - { - string memKey = $"UaFlix:filminfo:{filmUrl}"; - if (_hybridCache.TryGetValue(memKey, out FilmInfo res)) - return res; - - try - { - var headers = new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }; - if (IsNotAllowedHost(filmUrl)) - return null; - - var filmHtml = await Http.Get(filmUrl, headers: headers, proxy: _proxyManager.Get()); - var doc = new HtmlDocument(); - doc.LoadHtml(filmHtml); - - var result = new FilmInfo - { - Url = filmUrl - }; - - var titleNode = doc.DocumentNode.SelectSingleNode("//h1[@class='h1-title']"); - if (titleNode != null) - { - result.Title = titleNode.InnerText.Trim(); - } - - var metaDuration = doc.DocumentNode.SelectSingleNode("//meta[@property='og:video:duration']"); - if (metaDuration != null) - { - string durationStr = metaDuration.GetAttributeValue("content", ""); - if (int.TryParse(durationStr, out int duration)) - { - result.Duration = duration; - } - } - - var metaActors = doc.DocumentNode.SelectSingleNode("//meta[@property='og:video:actor']"); - if (metaActors != null) - { - string actorsStr = metaActors.GetAttributeValue("content", ""); - result.Actors = actorsStr.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.Trim()) - .ToList(); - } - - var metaDirector = doc.DocumentNode.SelectSingleNode("//meta[@property='og:video:director']"); - if (metaDirector != null) - { - result.Director = metaDirector.GetAttributeValue("content", ""); - } - - var descNode = doc.DocumentNode.SelectSingleNode("//div[@id='main-descr']//div[@itemprop='description']"); - if (descNode != null) - { - result.Description = descNode.InnerText.Trim(); - } - - var posterNode = doc.DocumentNode.SelectSingleNode("//img[@itemprop='image']"); - if (posterNode != null) - { - result.PosterUrl = posterNode.GetAttributeValue("src", ""); - if (!result.PosterUrl.StartsWith("http") && !string.IsNullOrEmpty(result.PosterUrl)) - { - result.PosterUrl = _init.host + result.PosterUrl; - } - } - - _hybridCache.Set(memKey, result, cacheTime(60)); - return result; - } - catch (Exception ex) - { - _onLog($"UaFlix GetFilmInfo error: {ex.Message}"); - } - return null; - } - - public async Task GetPaginationInfo(string filmUrl) - { - string memKey = $"UaFlix:pagination:{filmUrl}"; - if (_hybridCache.TryGetValue(memKey, out PaginationInfo res)) - return res; - - try - { - var headers = new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }; - if (IsNotAllowedHost(filmUrl)) - return null; - - var filmHtml = await Http.Get(filmUrl, headers: headers, proxy: _proxyManager.Get()); - var filmDoc = new HtmlDocument(); - filmDoc.LoadHtml(filmHtml); - - var paginationInfo = new PaginationInfo - { - SerialUrl = filmUrl - }; - - var allEpisodes = new List(); - var seasonUrls = new HashSet(); - - var seasonNodes = filmDoc.DocumentNode.SelectNodes("//div[contains(@class, 'sez-wr')]//a"); - if (seasonNodes == null) - seasonNodes = filmDoc.DocumentNode.SelectNodes("//div[contains(@class, 'fss-box')]//a"); - if (seasonNodes != null && seasonNodes.Count > 0) - { - foreach (var node in seasonNodes) - { - string pageUrl = node.GetAttributeValue("href", null); - if (!string.IsNullOrEmpty(pageUrl)) - { - if (!pageUrl.StartsWith("http")) - pageUrl = _init.host + pageUrl; - - seasonUrls.Add(pageUrl); - } - } - } - else - { - seasonUrls.Add(filmUrl); - } - - var safeSeasonUrls = seasonUrls.Where(url => !IsNotAllowedHost(url)).ToList(); - if (safeSeasonUrls.Count == 0) - return null; - - var seasonTasks = safeSeasonUrls.Select(url => Http.Get(url, headers: headers, proxy: _proxyManager.Get()).AsTask()); - var seasonPagesHtml = await Task.WhenAll(seasonTasks); - - foreach (var html in seasonPagesHtml) - { - var pageDoc = new HtmlDocument(); - pageDoc.LoadHtml(html); - - var episodeNodes = pageDoc.DocumentNode.SelectNodes("//div[contains(@class, 'frels')]//a[contains(@class, 'vi-img')]"); - if (episodeNodes != null) - { - foreach (var episodeNode in episodeNodes) - { - string episodeUrl = episodeNode.GetAttributeValue("href", ""); - if (!episodeUrl.StartsWith("http")) - episodeUrl = _init.host + episodeUrl; - - var match = Regex.Match(episodeUrl, @"season-(\d+).*?episode-(\d+)"); - if (match.Success) - { - allEpisodes.Add(new EpisodeLinkInfo - { - url = episodeUrl, - title = episodeNode.SelectSingleNode(".//div[@class='vi-rate']")?.InnerText.Trim() ?? $"Епізод {match.Groups[2].Value}", - season = int.Parse(match.Groups[1].Value), - episode = int.Parse(match.Groups[2].Value) - }); - } - } - } - } - - paginationInfo.Episodes = allEpisodes.OrderBy(e => e.season).ThenBy(e => e.episode).ToList(); - - if (paginationInfo.Episodes.Any()) - { - var uniqueSeasons = paginationInfo.Episodes.Select(e => e.season).Distinct().OrderBy(se => se); - foreach (var season in uniqueSeasons) - { - paginationInfo.Seasons[season] = 1; - } - } - - if (paginationInfo.Episodes.Count > 0) - { - _hybridCache.Set(memKey, paginationInfo, cacheTime(20)); - return paginationInfo; - } - } - catch (Exception ex) - { - _onLog($"UaFlix GetPaginationInfo error: {ex.Message}"); - } - return null; - } - - public async Task ParseEpisode(string url) - { - var result = new Uaflix.Models.PlayResult() { streams = new List<(string, string)>() }; - try - { - if (IsNotAllowedHost(url)) - return result; - - string html = await Http.Get(url, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get()); - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - var videoNode = doc.DocumentNode.SelectSingleNode("//video"); - if (videoNode != null) - { - string videoUrl = videoNode.GetAttributeValue("src", ""); - if (!string.IsNullOrEmpty(videoUrl)) - { - result.streams.Add((videoUrl, "1080p")); - return result; - } - } - - var iframe = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'video-box')]//iframe"); - if (iframe != null) - { - string iframeUrl = iframe.GetAttributeValue("src", "").Replace("&", "&"); - if (iframeUrl.StartsWith("//")) - iframeUrl = "https:" + iframeUrl; - - if (iframeUrl.Contains("ashdi.vip/serial/")) - { - result.ashdi_url = iframeUrl; - return result; - } - - // Ігноруємо YouTube трейлери - if (iframeUrl.Contains("youtube.com/embed/")) - { - _onLog($"ParseEpisode: Ignoring YouTube trailer iframe: {iframeUrl}"); - return result; - } - - if (iframeUrl.Contains("zetvideo.net")) - result.streams = await ParseAllZetvideoSources(iframeUrl); - else if (iframeUrl.Contains("ashdi.vip")) - { - // Перевіряємо, чи це ashdi-vod (окремий епізод) або ashdi-serial (багатосерійний плеєр) - if (iframeUrl.Contains("/vod/")) - { - // Це окремий епізод на ashdi.vip/vod/, обробляємо як ashdi-vod - var (file, voiceName) = await ParseAshdiVodEpisode(iframeUrl); - if (!string.IsNullOrEmpty(file)) - { - result.streams.Add((file, "1080p")); - } - } - else - { - // Це багатосерійний плеєр, обробляємо як і раніше - result.streams = await ParseAllAshdiSources(iframeUrl); - var idMatch = Regex.Match(iframeUrl, @"_(\d+)|vod/(\d+)"); - if (idMatch.Success) - { - string ashdiId = idMatch.Groups[1].Success ? idMatch.Groups[1].Value : idMatch.Groups[2].Value; - result.subtitles = await GetAshdiSubtitles(ashdiId); - } - } - } - } - } - catch (Exception ex) - { - _onLog($"ParseEpisode error: {ex.Message}"); - } - _onLog($"ParseEpisode result: streams.count={result.streams.Count}, ashdi_url={result.ashdi_url}"); - return result; - } - - private void NormalizeUaflixVoiceNames(SerialAggregatedStructure structure) - { - const string baseName = "Uaflix"; - const string zetName = "Uaflix #2"; - const string ashdiName = "Uaflix #3"; - - if (structure == null || structure.Voices == null || structure.Voices.Count == 0) - return; - - bool hasBase = structure.Voices.ContainsKey(baseName); - bool hasZet = structure.Voices.ContainsKey(zetName); - bool hasAshdi = structure.Voices.ContainsKey(ashdiName); - - if (hasBase) - return; - - if (hasZet && !hasAshdi) - { - var voice = structure.Voices[zetName]; - voice.DisplayName = baseName; - structure.Voices.Remove(zetName); - structure.Voices[baseName] = voice; - } - else if (hasAshdi && !hasZet) - { - var voice = structure.Voices[ashdiName]; - voice.DisplayName = baseName; - structure.Voices.Remove(ashdiName); - structure.Voices[baseName] = voice; - } - } - - async Task> ParseAllZetvideoSources(string iframeUrl) - { - var result = new List<(string link, string quality)>(); - if (IsNotAllowedHost(iframeUrl)) - return result; - - var html = await Http.Get(iframeUrl, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://zetvideo.net/") }, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(html)) return result; - - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - var script = doc.DocumentNode.SelectSingleNode("//script[contains(text(), 'file:')]"); - if (script != null) - { - var match = Regex.Match(script.InnerText, @"file:\s*""([^""]+\.m3u8)"); - if (match.Success) - { - result.Add((match.Groups[1].Value, "1080p")); - return result; - } - } - - var sourceNodes = doc.DocumentNode.SelectNodes("//source[contains(@src, '.m3u8')]"); - if (sourceNodes != null) - { - foreach (var node in sourceNodes) - { - result.Add((node.GetAttributeValue("src", null), node.GetAttributeValue("label", null) ?? node.GetAttributeValue("res", null) ?? "1080p")); - } - } - return result; - } - - async Task> ParseAllAshdiSources(string iframeUrl) - { - var result = new List<(string link, string quality)>(); - if (IsNotAllowedHost(iframeUrl)) - return result; - - var html = await Http.Get(iframeUrl, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://ashdi.vip/") }, proxy: _proxyManager.Get()); - if (string.IsNullOrEmpty(html)) return result; - - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - var sourceNodes = doc.DocumentNode.SelectNodes("//source[contains(@src, '.m3u8')]"); - if (sourceNodes != null) - { - foreach (var node in sourceNodes) - { - result.Add((node.GetAttributeValue("src", null), node.GetAttributeValue("label", null) ?? node.GetAttributeValue("res", null) ?? "1080p")); - } - } - return result; - } - - async Task GetAshdiSubtitles(string id) - { - string url = $"https://ashdi.vip/vod/{id}"; - if (IsNotAllowedHost(url)) - return null; - - var html = await Http.Get(url, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://ashdi.vip/") }, proxy: _proxyManager.Get()); - string subtitle = new Regex("subtitle(\")?:\"([^\"]+)\"").Match(html).Groups[2].Value; - if (!string.IsNullOrEmpty(subtitle)) - { - var match = new Regex("\\[([^\\]]+)\\](https?://[^\\,]+)").Match(subtitle); - var st = new Shared.Models.Templates.SubtitleTpl(); - while (match.Success) - { - st.Append(match.Groups[1].Value, match.Groups[2].Value); - match = match.NextMatch(); - } - if (!st.IsEmpty()) - return st; - } - return null; - } - - public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) - { - if (init != null && init.rhub && rhub != -1) - return TimeSpan.FromMinutes(rhub); - - int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home; - if (ctime > multiaccess) - ctime = multiaccess; - - return TimeSpan.FromMinutes(ctime); - } - - private static bool IsNotAllowedHost(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - return false; - - return NotAllowedHosts.Contains(uri.Host); - } - - /// - /// Оновлений метод кешування згідно стандарту Lampac - /// - public static TimeSpan GetCacheTime(OnlinesSettings init, int multiaccess = 20, int home = 5, int mikrotik = 2, int rhub = -1) - { - if (init != null && init.rhub && rhub != -1) - return TimeSpan.FromMinutes(rhub); - - int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home; - if (init != null && ctime > init.cache_time && init.cache_time > 0) - ctime = init.cache_time; - - return TimeSpan.FromMinutes(ctime); - } - } -} diff --git a/Uaflix/manifest.json b/Uaflix/manifest.json deleted file mode 100644 index 87d6ce9..0000000 --- a/Uaflix/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "enable": true, - "version": 2, - "initspace": "Uaflix.ModInit", - "online": "Uaflix.OnlineApi" -} \ No newline at end of file diff --git a/Unimay/Controllers/Controller.cs b/Unimay/Controllers/Controller.cs deleted file mode 100644 index 05138d8..0000000 --- a/Unimay/Controllers/Controller.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Shared.Engine; -using System; -using System.Threading.Tasks; -using System.Linq; -using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; -using System.Web; -using Newtonsoft.Json.Linq; -using Shared.Models.Templates; -using Shared.Models.Online.Settings; -using Shared; - -namespace Unimay.Controllers -{ - public class Controller : BaseOnlineController - { - ProxyManager proxyManager; - - public Controller() - { - proxyManager = new ProxyManager(ModInit.Unimay); - } - - [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) - { - var init = await loadKit(ModInit.Unimay); - if (await IsBadInitialization(init, rch: false)) - return badInitMsg; - - var invoke = new UnimayInvoke(init, hybridCache, OnLog, proxyManager); - - if (!string.IsNullOrEmpty(code)) - { - // Fetch release details - return await Release(invoke, init, code, title, original_title, serial, s, e, play, rjson); - } - else - { - // Search - return await Search(invoke, init, title, original_title, serial, rjson); - } - } - - async ValueTask Search(UnimayInvoke invoke, OnlinesSettings init, string title, string original_title, int serial, bool rjson) - { - string memKey = $"unimay:search:{title}:{original_title}:{serial}"; - - return await InvkSemaphore(init, memKey, async () => - { - var searchResults = await invoke.Search(title, original_title, serial); - if (searchResults == null || searchResults.Content.Count == 0) - return OnError("no results"); - - var stpl = new SimilarTpl(searchResults.Content.Count); - var results = invoke.GetSearchResults(host, searchResults, title, original_title, serial); - - foreach (var (itemTitle, itemYear, itemType, releaseUrl) in results) - { - stpl.Append(itemTitle, itemYear, itemType, releaseUrl); - } - - return ContentTo(rjson ? stpl.ToJson() : stpl.ToHtml()); - }); - } - - async ValueTask Release(UnimayInvoke invoke, OnlinesSettings init, string code, string title, string original_title, int serial, int s, int e, bool play, bool rjson) - { - string memKey = $"unimay:release:{code}"; - - return await InvkSemaphore(init, memKey, async () => - { - var releaseDetail = await invoke.Release(code); - if (releaseDetail == null) - return OnError("no release detail"); - - string itemType = releaseDetail.Type; - var playlist = releaseDetail.Playlist; - - if (playlist == null || playlist.Count == 0) - return OnError("no playlist"); - - if (play) - { - // Get specific episode - Unimay.Models.Episode episode = null; - if (itemType == "Телесеріал") - { - if (s <= 0 || e <= 0) return OnError("invalid episode"); - episode = playlist.FirstOrDefault(ep => ep.Number == e); - } - else // Movie - { - episode = playlist[0]; - } - - if (episode == null) - return OnError("episode not found"); - - string masterUrl = invoke.GetStreamUrl(episode); - if (string.IsNullOrEmpty(masterUrl)) - return OnError("no stream"); - - return Redirect(HostStreamProxy(init, accsArgs(masterUrl), proxy: proxyManager.Get())); - } - - if (itemType == "Фільм") - { - var (movieTitle, movieLink) = invoke.GetMovieResult(host, releaseDetail, title, original_title); - var mtpl = new MovieTpl(title, original_title, 1); - mtpl.Append(movieTitle, accsArgs(movieLink), method: "play"); - return ContentTo(rjson ? mtpl.ToJson() : mtpl.ToHtml()); - } - else if (itemType == "Телесеріал") - { - if (s == -1) - { - // Assume single season - var (seasonName, seasonUrl, seasonId) = invoke.GetSeasonInfo(host, code, title, original_title); - var stpl = new SeasonTpl(); - stpl.Append(seasonName, seasonUrl, seasonId); - return ContentTo(rjson ? stpl.ToJson() : stpl.ToHtml()); - } - else - { - // Episodes for season 1 - var episodes = invoke.GetEpisodesForSeason(host, releaseDetail, title, original_title); - var mtpl = new MovieTpl(title, original_title, episodes.Count); - foreach (var (epTitle, epLink) in episodes) - { - mtpl.Append(epTitle, accsArgs(epLink), method: "play"); - } - return ContentTo(rjson ? mtpl.ToJson() : mtpl.ToHtml()); - } - } - - return OnError("unsupported type"); - }); - } - } -} \ No newline at end of file diff --git a/Unimay/ModInit.cs b/Unimay/ModInit.cs deleted file mode 100644 index e222a62..0000000 --- a/Unimay/ModInit.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Newtonsoft.Json; -using Shared; -using Shared.Engine; -using Newtonsoft.Json.Linq; -using Shared; -using Shared.Models.Online.Settings; -using Shared.Models.Module; - -using Newtonsoft.Json; -using Shared; -using Shared.Engine; -using Newtonsoft.Json.Linq; - -namespace Unimay -{ - public class ModInit - { - public static OnlinesSettings Unimay; - - /// - /// модуль загружен - /// - public static void loaded(InitspaceModel initspace) - { - Unimay = new OnlinesSettings("Unimay", "https://api.unimay.media/v1", streamproxy: false, useproxy: false) - { - displayname = "Unimay", - displayindex = 0, - proxy = new Shared.Models.Base.ProxySettings() - { - useAuth = true, - username = "a", - password = "a", - list = new string[] { "socks5://IP:PORT" } - } - }; - Unimay = ModuleInvoke.Conf("Unimay", Unimay).ToObject(); - - // Виводити "уточнити пошук" - AppInit.conf.online.with_search.Add("unimay"); - } - } -} \ No newline at end of file diff --git a/Unimay/Models/Episode.cs b/Unimay/Models/Episode.cs deleted file mode 100644 index f837ebf..0000000 --- a/Unimay/Models/Episode.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Unimay.Models -{ - public class Episode - { - [JsonPropertyName("number")] - public int Number { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("hls")] - public Hls Hls { get; set; } - } - - public class Hls - { - [JsonPropertyName("master")] - public string Master { get; set; } - } -} \ No newline at end of file diff --git a/Unimay/Models/ReleaseResponse.cs b/Unimay/Models/ReleaseResponse.cs deleted file mode 100644 index c9e6bd4..0000000 --- a/Unimay/Models/ReleaseResponse.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Unimay.Models -{ - public class ReleaseResponse - { - [JsonPropertyName("code")] - public string Code { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("year")] - public string Year { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } // "Фільм" або "Телесеріал" - - [JsonPropertyName("playlist")] - public List Playlist { get; set; } - } -} \ No newline at end of file diff --git a/Unimay/Models/SearchResponse.cs b/Unimay/Models/SearchResponse.cs deleted file mode 100644 index 615278c..0000000 --- a/Unimay/Models/SearchResponse.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Unimay.Models -{ - public class SearchResponse - { - [JsonPropertyName("content")] - public List Content { get; set; } - - [JsonPropertyName("totalElements")] - public int TotalElements { get; set; } - } - - public class ReleaseInfo - { - [JsonPropertyName("code")] - public string Code { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("year")] - public string Year { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } // "Фільм" або "Телесеріал" - - [JsonPropertyName("names")] - public Names Names { get; set; } - } - - public class Names - { - [JsonPropertyName("ukr")] - public string Ukr { get; set; } - - [JsonPropertyName("eng")] - public string Eng { get; set; } - } -} \ No newline at end of file diff --git a/Unimay/OnlineApi.cs b/Unimay/OnlineApi.cs deleted file mode 100644 index c486aee..0000000 --- a/Unimay/OnlineApi.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Shared.Models.Base; -using System.Collections.Generic; - -namespace Unimay -{ - 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.Unimay; - - // Визначення isAnime згідно стандарту Lampac (Deepwiki): - // isanime = true якщо original_language == "ja" або "zh" - bool hasLang = !string.IsNullOrEmpty(original_language); - bool isanime = hasLang && (original_language == "ja" || original_language == "zh"); - - // Unimay — аніме-провайдер. Додаємо якщо: - // - загальний пошук (serial == -1), або - // - контент є аніме (isanime), або - // - мова невідома (немає original_language) - if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang)) - { - string url = init.overridehost; - if (string.IsNullOrEmpty(url)) - url = $"{host}/unimay"; - - online.Add((init.displayname, url, "unimay", init.displayindex)); - } - - return online; - } - } -} \ No newline at end of file diff --git a/Unimay/Unimay.csproj b/Unimay/Unimay.csproj deleted file mode 100644 index 9ced83e..0000000 --- a/Unimay/Unimay.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net8.0 - enable - enable - false - latest - - - - - ..\..\Shared.dll - - - - \ No newline at end of file diff --git a/Unimay/UnimayInvoke.cs b/Unimay/UnimayInvoke.cs deleted file mode 100644 index e8a1277..0000000 --- a/Unimay/UnimayInvoke.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Shared; -using Shared.Models.Online.Settings; -using Shared.Models; -using System.Linq; -using Unimay.Models; -using Shared.Engine; -using System.Net; -using System.Text; - -namespace Unimay -{ - public class UnimayInvoke - { - private static readonly HashSet NotAllowedHosts = - new HashSet( - new[] - { - "c3ZpdGFubW92aWU=", - "cG9ydGFsLXR2", - } - .Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))), - StringComparer.OrdinalIgnoreCase - ); - private OnlinesSettings _init; - private ProxyManager _proxyManager; - private HybridCache _hybridCache; - private Action _onLog; - - public UnimayInvoke(OnlinesSettings init, HybridCache hybridCache, Action onLog, ProxyManager proxyManager) - { - _init = init; - _hybridCache = hybridCache; - _onLog = onLog; - _proxyManager = proxyManager; - } - - public async Task Search(string title, string original_title, int serial) - { - string memKey = $"unimay:search:{title}:{original_title}:{serial}"; - if (_hybridCache.TryGetValue(memKey, out SearchResponse searchResults)) - return searchResults; - - try - { - string searchQuery = System.Web.HttpUtility.UrlEncode(title ?? original_title ?? ""); - string searchUrl = $"{_init.host}/release/search?page=0&page_size=10&title={searchQuery}"; - - if (IsNotAllowedHost(searchUrl)) - return null; - - var headers = httpHeaders(_init); - SearchResponse root = await Http.Get(searchUrl, timeoutSeconds: 8, proxy: _proxyManager.Get(), headers: headers); - - if (root == null || root.Content == null || root.Content.Count == 0) - { - // Refresh proxy on failure - _proxyManager.Refresh(); - return null; - } - - _hybridCache.Set(memKey, root, cacheTime(30, init: _init)); - return root; - } - catch (Exception ex) - { - _onLog($"Unimay search error: {ex.Message}"); - return null; - } - } - - public async Task Release(string code) - { - string memKey = $"unimay:release:{code}"; - if (_hybridCache.TryGetValue(memKey, out ReleaseResponse releaseDetail)) - return releaseDetail; - - try - { - string releaseUrl = $"{_init.host}/release?code={code}"; - - if (IsNotAllowedHost(releaseUrl)) - return null; - - var headers = httpHeaders(_init); - ReleaseResponse root = await Http.Get(releaseUrl, timeoutSeconds: 8, proxy: _proxyManager.Get(), headers: headers); - - if (root == null) - { - // Refresh proxy on failure - _proxyManager.Refresh(); - return null; - } - - _hybridCache.Set(memKey, root, cacheTime(60, init: _init)); - return root; - } - catch (Exception ex) - { - _onLog($"Unimay release error: {ex.Message}"); - return null; - } - } - - public List<(string title, string year, string type, string url)> GetSearchResults(string host, SearchResponse searchResults, string title, string original_title, int serial) - { - var results = new List<(string title, string year, string type, string url)>(); - - foreach (var item in searchResults.Content) - { - // Filter by serial if specified (0: movie "Фільм", 1: serial "Телесеріал") - if (serial != -1) - { - bool isMovie = item.Type == "Фільм"; - if ((serial == 0 && !isMovie) || (serial == 1 && isMovie)) - continue; - } - - string itemTitle = item.Names?.Ukr ?? item.Names?.Eng ?? item.Title; - string releaseUrl = $"{host}/unimay?code={item.Code}&title={System.Web.HttpUtility.UrlEncode(itemTitle)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial={serial}"; - results.Add((itemTitle, item.Year, item.Type, releaseUrl)); - } - - return results; - } - - public (string title, string link) GetMovieResult(string host, ReleaseResponse releaseDetail, string title, string original_title) - { - if (releaseDetail.Playlist == null || releaseDetail.Playlist.Count == 0) - return (null, null); - - var movieEpisode = releaseDetail.Playlist[0]; - string movieLink = $"{host}/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=0&play=true"; - string movieTitle = movieEpisode.Title ?? title; - - return (movieTitle, movieLink); - } - - public (string seasonName, string seasonUrl, string seasonId) GetSeasonInfo(string host, string code, string title, string original_title) - { - string seasonUrl = $"{host}/unimay?code={code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1"; - return ("Сезон 1", seasonUrl, "1"); - } - - public List<(string episodeTitle, string episodeUrl)> GetEpisodesForSeason(string host, ReleaseResponse releaseDetail, string title, string original_title) - { - var episodes = new List<(string episodeTitle, string episodeUrl)>(); - - if (releaseDetail.Playlist == null) - return episodes; - - foreach (var ep in releaseDetail.Playlist.Where(ep => ep.Number >= 1 && ep.Number <= 24).OrderBy(ep => ep.Number)) - { - string epTitle = ep.Title ?? $"Епізод {ep.Number}"; - string epLink = $"{host}/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1&e={ep.Number}&play=true"; - episodes.Add((epTitle, epLink)); - } - - return episodes; - } - - public string GetStreamUrl(Episode episode) - { - return episode.Hls?.Master; - } - - private List httpHeaders(OnlinesSettings init) - { - return new List() - { - new HeadersModel("User-Agent", "Mozilla/5.0"), - new HeadersModel("Referer", init.host), - new HeadersModel("Accept", "application/json") - }; - } - - private static bool IsNotAllowedHost(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - return false; - - return NotAllowedHosts.Contains(uri.Host); - } - - public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) - { - if (init != null && init.rhub && rhub != -1) - return TimeSpan.FromMinutes(rhub); - - int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home; - if (ctime > multiaccess) - ctime = multiaccess; - - return TimeSpan.FromMinutes(ctime); - } - } -} diff --git a/Unimay/manifest.json b/Unimay/manifest.json deleted file mode 100644 index c701751..0000000 --- a/Unimay/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "enable": true, - "version": 2, - "initspace": "Unimay.ModInit", - "online": "Unimay.OnlineApi" -} \ No newline at end of file