From 7a211c838bf9249c67eccdb01c4c352d7957728f Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 20 Sep 2025 11:47:28 +0300 Subject: [PATCH] Refactor --- AnimeON/AnimeONInvoke.cs | 148 ++++++++++++++ AnimeON/Controller.cs | 73 ++++++- AnimeON/ModInit.cs | 9 +- AnimeON/Models/EmbedModel.cs | 23 +++ AnimeON/Models/Models.cs | 3 + AnimeON/Models/Serial.cs | 29 +++ AnimeON/Models/Voice.cs | 26 +++ AnimeON/manifest.json | 2 +- CikavaIdeya/CikavaIdeyaInvoke.cs | 312 +++++++++++++++++++++++++++++ CikavaIdeya/Controller.cs | 8 +- CikavaIdeya/ModInit.cs | 23 ++- CikavaIdeya/Models/EpisodeModel.cs | 16 ++ CikavaIdeya/Models/PlayerModel.cs | 17 ++ CikavaIdeya/Models/SeasonModel.cs | 14 ++ CikavaIdeya/manifest.json | 2 +- Uaflix/Controller.cs | 45 ++--- Uaflix/ModInit.cs | 19 +- Uaflix/Models/EpisodeLinkInfo.cs | 12 ++ Uaflix/Models/PlayResult.cs | 12 ++ Uaflix/UaflixInvoke.cs | 250 +++++++++++++++++++++++ Uaflix/manifest.json | 2 +- Unimay/Controllers/Controller.cs | 104 +++------- Unimay/ModInit.cs | 5 +- Unimay/Models/Episode.cs | 22 ++ Unimay/Models/ReleaseResponse.cs | 23 +++ Unimay/Models/SearchResponse.cs | 41 ++++ Unimay/Unimay.csproj | 4 +- Unimay/UnimayInvoke.cs | 173 ++++++++++++++++ Unimay/manifest.json | 2 +- 29 files changed, 1266 insertions(+), 153 deletions(-) create mode 100644 AnimeON/AnimeONInvoke.cs create mode 100644 AnimeON/Models/EmbedModel.cs create mode 100644 AnimeON/Models/Serial.cs create mode 100644 AnimeON/Models/Voice.cs create mode 100644 CikavaIdeya/CikavaIdeyaInvoke.cs create mode 100644 CikavaIdeya/Models/EpisodeModel.cs create mode 100644 CikavaIdeya/Models/PlayerModel.cs create mode 100644 CikavaIdeya/Models/SeasonModel.cs create mode 100644 Uaflix/Models/EpisodeLinkInfo.cs create mode 100644 Uaflix/Models/PlayResult.cs create mode 100644 Uaflix/UaflixInvoke.cs create mode 100644 Unimay/Models/Episode.cs create mode 100644 Unimay/Models/ReleaseResponse.cs create mode 100644 Unimay/Models/SearchResponse.cs create mode 100644 Unimay/UnimayInvoke.cs diff --git a/AnimeON/AnimeONInvoke.cs b/AnimeON/AnimeONInvoke.cs new file mode 100644 index 0000000..6e32bb1 --- /dev/null +++ b/AnimeON/AnimeONInvoke.cs @@ -0,0 +1,148 @@ +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 AnimeON.Models; +using Shared.Engine; + +namespace AnimeON +{ + public class AnimeONInvoke + { + 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)}"; + 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/fundubs/{animeId}"; + 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); + return fundubsResponse?.FunDubs; + } + + public async Task GetEpisodes(int animeId, int playerId, int fundubId) + { + string episodesUrl = $"{_init.host}/api/player/episodes/{animeId}?take=100&skip=-1&playerId={playerId}&fundubId={fundubId}"; + string episodesJson = await Http.Get(episodesUrl, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, 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/") + }; + + 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 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); + } + } +} \ No newline at end of file diff --git a/AnimeON/Controller.cs b/AnimeON/Controller.cs index 635a7bf..23085d3 100644 --- a/AnimeON/Controller.cs +++ b/AnimeON/Controller.cs @@ -33,14 +33,16 @@ namespace AnimeON.Controllers if (!init.enable) return Forbid(); - var seasons = await search(init, imdb_id, kinopoisk_id, title, original_title, year); + var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager); + + var seasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year); if (seasons == null || seasons.Count == 0) return Content("AnimeON", "text/html; charset=utf-8"); var allOptions = new List<(SearchModel season, FundubModel fundub, Player player)>(); foreach (var season in seasons) { - var fundubs = await GetFundubs(init, season.Id); + var fundubs = await invoke.GetFundubs(season.Id); if (fundubs != null) { foreach (var fundub in fundubs) @@ -76,7 +78,7 @@ namespace AnimeON.Controllers return Content("AnimeON", "text/html; charset=utf-8"); var selected = allOptions[s]; - var episodesData = await GetEpisodes(init, selected.season.Id, selected.player.Id, selected.fundub.Fundub.Id); + var episodesData = await invoke.GetEpisodes(selected.season.Id, selected.player.Id, selected.fundub.Fundub.Id); if (episodesData == null || episodesData.Episodes == null) return Content("AnimeON", "text/html; charset=utf-8"); @@ -85,9 +87,31 @@ namespace AnimeON.Controllers { var streamquality = new StreamQualityTpl(); string streamLink = !string.IsNullOrEmpty(ep.Hls) ? ep.Hls : ep.VideoUrl; - streamquality.Append(HostStreamProxy(init, streamLink), "hls"); - movie_tpl.Append(string.IsNullOrEmpty(ep.Name) ? $"Серія {ep.EpisodeNum}" : ep.Name, streamquality.Firts().link, streamquality: streamquality); + + if (selected.player.Name.ToLower() == "moon" && !string.IsNullOrEmpty(streamLink) && streamLink.Contains("moonanime.art/iframe/")) + { + streamLink = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}"; + streamquality.Append(streamLink, "hls"); + movie_tpl.Append(string.IsNullOrEmpty(ep.Name) ? $"Серія {ep.EpisodeNum}" : ep.Name, streamLink, streamquality: streamquality); + } + else if (!string.IsNullOrEmpty(streamLink)) + { + streamquality.Append(HostStreamProxy(init, streamLink), "hls"); + movie_tpl.Append(string.IsNullOrEmpty(ep.Name) ? $"Серія {ep.EpisodeNum}" : ep.Name, streamquality.Firts().link, streamquality: streamquality); + } } + + if (!string.IsNullOrEmpty(episodesData.AnotherPlayer) && episodesData.AnotherPlayer.Contains("ashdi.vip")) + { + var match = Regex.Match(episodesData.AnotherPlayer, "/serial/([0-9]+)"); + if (match.Success) + { + string ashdi_kp = match.Groups[1].Value; + string ashdi_link = $"/ashdi?kinopoisk_id={ashdi_kp}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}"; + movie_tpl.Append("Плеєр Ashdi", ashdi_link); + } + } + return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8"); } } @@ -96,16 +120,26 @@ namespace AnimeON.Controllers var tpl = new MovieTpl(title, original_title, allOptions.Count); foreach (var item in allOptions) { - var episodesData = await GetEpisodes(init, item.season.Id, item.player.Id, item.fundub.Fundub.Id); + var episodesData = await invoke.GetEpisodes(item.season.Id, item.player.Id, item.fundub.Fundub.Id); if (episodesData == null || episodesData.Episodes == null || episodesData.Episodes.Count == 0) continue; - + string translationName = $"[{item.player.Name}] {item.fundub.Fundub.Name}"; var streamquality = new StreamQualityTpl(); - var firstEp = episodesData.Episodes.First(); + var firstEp = episodesData.Episodes.FirstOrDefault(); string streamLink = !string.IsNullOrEmpty(firstEp.Hls) ? firstEp.Hls : firstEp.VideoUrl; - streamquality.Append(HostStreamProxy(init, streamLink), "hls"); - tpl.Append(translationName, streamquality.Firts().link, streamquality: streamquality); + + if (item.player.Name.ToLower() == "moon" && !string.IsNullOrEmpty(streamLink) && streamLink.Contains("moonanime.art/iframe/")) + { + streamLink = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}"; + streamquality.Append(streamLink, "hls"); + tpl.Append(translationName, streamLink, streamquality: streamquality); + } + else if (!string.IsNullOrEmpty(streamLink)) + { + streamquality.Append(HostStreamProxy(init, streamLink), "hls"); + tpl.Append(translationName, streamquality.Firts().link, streamquality: streamquality); + } } return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8"); } @@ -188,5 +222,24 @@ namespace AnimeON.Controllers return null; } + + [HttpGet("animeon/play")] + public async Task Play(string url) + { + if (string.IsNullOrEmpty(url)) + return OnError("url is empty"); + + var init = await loadKit(ModInit.AnimeON); + if (!init.enable) + return Forbid(); + + var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager); + string streamLink = await invoke.ParseMoonAnimePage(url); + + if (string.IsNullOrEmpty(streamLink)) + return Content("Не вдалося отримати посилання на відео", "text/html; charset=utf-8"); + + return Redirect(HostStreamProxy(init, streamLink)); + } } } diff --git a/AnimeON/ModInit.cs b/AnimeON/ModInit.cs index c8e9d3a..4c8b2b3 100644 --- a/AnimeON/ModInit.cs +++ b/AnimeON/ModInit.cs @@ -1,6 +1,7 @@ -using Shared; -using Shared.Models.Online.Settings; - +using Shared; +using Shared.Models.Online.Settings; +using Shared.Models.Module; + namespace AnimeON { public class ModInit @@ -10,7 +11,7 @@ namespace AnimeON /// /// модуль загружен /// - public static void loaded() + public static void loaded(InitspaceModel initspace) { AnimeON = new OnlinesSettings("AnimeON", "https://animeon.club", streamproxy: false) { diff --git a/AnimeON/Models/EmbedModel.cs b/AnimeON/Models/EmbedModel.cs new file mode 100644 index 0000000..afd9d3c --- /dev/null +++ b/AnimeON/Models/EmbedModel.cs @@ -0,0 +1,23 @@ +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 index c45874e..e82e42f 100644 --- a/AnimeON/Models/Models.cs +++ b/AnimeON/Models/Models.cs @@ -70,6 +70,9 @@ namespace AnimeON.Models { [JsonPropertyName("episodes")] public List Episodes { get; set; } + + [JsonPropertyName("anotherPlayer")] + public string AnotherPlayer { get; set; } } public class Episode diff --git a/AnimeON/Models/Serial.cs b/AnimeON/Models/Serial.cs new file mode 100644 index 0000000..ed2392a --- /dev/null +++ b/AnimeON/Models/Serial.cs @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..4c25ce7 --- /dev/null +++ b/AnimeON/Models/Voice.cs @@ -0,0 +1,26 @@ +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/manifest.json b/AnimeON/manifest.json index 0c653cb..86348fc 100644 --- a/AnimeON/manifest.json +++ b/AnimeON/manifest.json @@ -1,6 +1,6 @@ { "enable": true, - "version": 1, + "version": 2, "initspace": "AnimeON.ModInit", "online": "AnimeON.OnlineApi" } \ No newline at end of file diff --git a/CikavaIdeya/CikavaIdeyaInvoke.cs b/CikavaIdeya/CikavaIdeyaInvoke.cs new file mode 100644 index 0000000..b931428 --- /dev/null +++ b/CikavaIdeya/CikavaIdeyaInvoke.cs @@ -0,0 +1,312 @@ +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 CikavaIdeya.Models; +using Shared.Engine; +using System.Linq; + +namespace CikavaIdeya +{ + public class CikavaIdeyaInvoke + { + 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) }; + + 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; + + // Отримуємо список епізодів (для фільмів - один епізод, для серіалів - всі епізоди) + 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")) + { + result.iframe_url = 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 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); + } + } +} \ No newline at end of file diff --git a/CikavaIdeya/Controller.cs b/CikavaIdeya/Controller.cs index f516e90..a0c6b12 100644 --- a/CikavaIdeya/Controller.cs +++ b/CikavaIdeya/Controller.cs @@ -32,7 +32,9 @@ namespace CikavaIdeya.Controllers if (!init.enable) return Forbid(); - var episodesInfo = await search(init, imdb_id, kinopoisk_id, title, original_title, year, serial == 0); + 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"); @@ -45,7 +47,7 @@ namespace CikavaIdeya.Controllers if (episode == null) return Content("CikavaIdeya", "text/html; charset=utf-8"); - var playResult = await ParseEpisode(init, episode.url); + var playResult = await invoke.ParseEpisode(episode.url); if (!string.IsNullOrEmpty(playResult.iframe_url)) { @@ -182,7 +184,7 @@ namespace CikavaIdeya.Controllers if (filmUrl == null) { - var firstNode = filmNodes.First().SelectSingleNode(".//a[@class='th-in']"); + var firstNode = filmNodes.FirstOrDefault()?.SelectSingleNode(".//a[@class='th-in']"); if (firstNode != null) filmUrl = firstNode.GetAttributeValue("href", ""); } diff --git a/CikavaIdeya/ModInit.cs b/CikavaIdeya/ModInit.cs index 17d71b2..86e97d6 100644 --- a/CikavaIdeya/ModInit.cs +++ b/CikavaIdeya/ModInit.cs @@ -1,24 +1,25 @@ -using Shared; -using Shared.Models.Online.Settings; - -namespace CikavaIdeya -{ - public class ModInit - { +using Shared; +using Shared.Models.Online.Settings; +using Shared.Models.Module; + +namespace CikavaIdeya +{ + public class ModInit + { public static OnlinesSettings CikavaIdeya; - + /// /// модуль загружен /// - public static void loaded() + public static void loaded(InitspaceModel initspace) { CikavaIdeya = new OnlinesSettings("CikavaIdeya", "https://cikava-ideya.top", streamproxy: false) { displayname = "ЦікаваІдея" }; - + // Виводити "уточнити пошук" 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 new file mode 100644 index 0000000..46d0a11 --- /dev/null +++ b/CikavaIdeya/Models/EpisodeModel.cs @@ -0,0 +1,16 @@ +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/PlayerModel.cs b/CikavaIdeya/Models/PlayerModel.cs new file mode 100644 index 0000000..3e871f9 --- /dev/null +++ b/CikavaIdeya/Models/PlayerModel.cs @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..570c7e0 --- /dev/null +++ b/CikavaIdeya/Models/SeasonModel.cs @@ -0,0 +1,14 @@ +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/manifest.json b/CikavaIdeya/manifest.json index af00898..f03df58 100644 --- a/CikavaIdeya/manifest.json +++ b/CikavaIdeya/manifest.json @@ -1,6 +1,6 @@ { "enable": true, - "version": 1, + "version": 2, "initspace": "CikavaIdeya.ModInit", "online": "CikavaIdeya.OnlineApi" } \ No newline at end of file diff --git a/Uaflix/Controller.cs b/Uaflix/Controller.cs index 6ec5108..b3b55f4 100644 --- a/Uaflix/Controller.cs +++ b/Uaflix/Controller.cs @@ -11,25 +11,10 @@ using Shared.Models.Templates; using System.Text.RegularExpressions; using Shared.Models.Online.Settings; using Shared.Models; +using Uaflix.Models; namespace Uaflix.Controllers { - #region 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 ashdi_url { get; set; } - public List<(string link, string quality)> streams { get; set; } - public SubtitleTpl? subtitles { get; set; } - } - #endregion public class Controller : BaseOnlineController { @@ -48,7 +33,9 @@ namespace Uaflix.Controllers if (!init.enable) return Forbid(); - var episodesInfo = await search(init, imdb_id, kinopoisk_id, title, original_title, year, serial == 0); + var invoke = new UaflixInvoke(init, hybridCache, OnLog, proxyManager); + + var episodesInfo = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial == 0); if (episodesInfo == null) return Content("Uaflix", "text/html; charset=utf-8"); @@ -61,7 +48,7 @@ namespace Uaflix.Controllers if (episode == null) return Content("Uaflix", "text/html; charset=utf-8"); - var playResult = await ParseEpisode(init, episode.url); + var playResult = await invoke.ParseEpisode(episode.url); if (!string.IsNullOrEmpty(playResult.ashdi_url)) { @@ -109,10 +96,10 @@ namespace Uaflix.Controllers } } - async ValueTask> search(OnlinesSettings init, string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool isfilm = false) + async ValueTask> search(OnlinesSettings init, string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool isfilm = false) { string memKey = $"UaFlix:search:{kinopoisk_id}:{imdb_id}"; - if (hybridCache.TryGetValue(memKey, out List res)) + if (hybridCache.TryGetValue(memKey, out List res)) return res; try @@ -143,14 +130,14 @@ namespace Uaflix.Controllers } if (filmUrl == null) - filmUrl = filmNodes.First().GetAttributeValue("href", ""); + filmUrl = filmNodes.FirstOrDefault()?.GetAttributeValue("href", ""); if (!filmUrl.StartsWith("http")) filmUrl = init.host + filmUrl; if (isfilm) { - res = new List() { new EpisodeLinkInfo() { url = filmUrl } }; + res = new List() { new Uaflix.Models.EpisodeLinkInfo() { url = filmUrl } }; hybridCache.Set(memKey, res, cacheTime(20)); return res; } @@ -158,11 +145,11 @@ namespace Uaflix.Controllers var filmHtml = await Http.Get(filmUrl, headers: headers); doc.LoadHtml(filmHtml); - res = new List(); + res = new List(); var episodeNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'frels2')]//a[contains(@class, 'vi-img')]"); if (episodeNodes != null) { - foreach (var episodeNode in episodeNodes.Reverse()) + foreach (var episodeNode in episodeNodes.Reverse().ToList()) { string episodeUrl = episodeNode.GetAttributeValue("href", ""); if (!episodeUrl.StartsWith("http")) @@ -171,7 +158,7 @@ namespace Uaflix.Controllers var match = Regex.Match(episodeUrl, @"season-(\d+).*?episode-(\d+)"); if (match.Success) { - res.Add(new EpisodeLinkInfo + res.Add(new Uaflix.Models.EpisodeLinkInfo { url = episodeUrl, title = episodeNode.SelectSingleNode(".//div[@class='vi-rate']")?.InnerText.Trim() ?? $"Епізод {match.Groups[2].Value}", @@ -187,7 +174,7 @@ namespace Uaflix.Controllers var iframe = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'video-box')]//iframe[contains(@src, 'ashdi.vip/serial/')]"); if (iframe != null) { - res.Add(new EpisodeLinkInfo() { url = filmUrl, season = 1, episode = 1 }); + res.Add(new Uaflix.Models.EpisodeLinkInfo() { url = filmUrl, season = 1, episode = 1 }); } } @@ -203,9 +190,9 @@ namespace Uaflix.Controllers return null; } - async Task ParseEpisode(OnlinesSettings init, string url) + async Task ParseEpisode(OnlinesSettings init, string url) { - var result = new PlayResult() { streams = new List<(string, string)>() }; + var result = new Uaflix.Models.PlayResult() { streams = new List<(string, string)>() }; try { string html = await Http.Get(url, headers: new List() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) }); @@ -305,7 +292,7 @@ namespace Uaflix.Controllers if (!string.IsNullOrEmpty(subtitle)) { var match = new Regex("\\[([^\\]]+)\\](https?://[^\\,]+)").Match(subtitle); - var st = new SubtitleTpl(); + var st = new Shared.Models.Templates.SubtitleTpl(); while (match.Success) { st.Append(match.Groups[1].Value, match.Groups[2].Value); diff --git a/Uaflix/ModInit.cs b/Uaflix/ModInit.cs index 5ddac34..4eed53d 100644 --- a/Uaflix/ModInit.cs +++ b/Uaflix/ModInit.cs @@ -1,16 +1,17 @@ -using Shared; -using Shared.Models.Online.Settings; - -namespace Uaflix -{ - public class ModInit - { +using Shared; +using Shared.Models.Online.Settings; +using Shared.Models.Module; + +namespace Uaflix +{ + public class ModInit + { public static OnlinesSettings UaFlix; /// /// модуль загружен /// - public static void loaded() + public static void loaded(InitspaceModel initspace) { UaFlix = new OnlinesSettings("Uaflix", "https://uafix.net", streamproxy: false) { @@ -20,5 +21,5 @@ namespace Uaflix // Виводити "уточнити пошук" AppInit.conf.online.with_search.Add("uaflix"); } - } + } } \ No newline at end of file diff --git a/Uaflix/Models/EpisodeLinkInfo.cs b/Uaflix/Models/EpisodeLinkInfo.cs new file mode 100644 index 0000000..4cd5986 --- /dev/null +++ b/Uaflix/Models/EpisodeLinkInfo.cs @@ -0,0 +1,12 @@ +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; } + } +} \ No newline at end of file diff --git a/Uaflix/Models/PlayResult.cs b/Uaflix/Models/PlayResult.cs new file mode 100644 index 0000000..2275af7 --- /dev/null +++ b/Uaflix/Models/PlayResult.cs @@ -0,0 +1,12 @@ +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/UaflixInvoke.cs b/Uaflix/UaflixInvoke.cs new file mode 100644 index 0000000..0e27eaa --- /dev/null +++ b/Uaflix/UaflixInvoke.cs @@ -0,0 +1,250 @@ +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; + +namespace Uaflix +{ + public class UaflixInvoke + { + 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; + } + + public async Task> Search(string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool isfilm = false) + { + string memKey = $"UaFlix:search:{kinopoisk_id}:{imdb_id}"; + if (_hybridCache.TryGetValue(memKey, out List res)) + return res; + + 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) }; + + 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')]"); + if (filmNodes == null) return null; + + string filmUrl = null; + foreach (var filmNode in filmNodes) + { + var h2Node = filmNode.SelectSingleNode(".//h2"); + if (h2Node == null || !h2Node.InnerText.Trim().ToLower().Contains(filmTitle.ToLower())) continue; + + var descNode = filmNode.SelectSingleNode(".//div[contains(@class, 'sres-desc')]"); + if (year > 0 && (descNode?.InnerText ?? "").Contains(year.ToString())) + { + filmUrl = filmNode.GetAttributeValue("href", ""); + break; + } + } + + if (filmUrl == null) + filmUrl = filmNodes.FirstOrDefault()?.GetAttributeValue("href", ""); + + if (!filmUrl.StartsWith("http")) + filmUrl = _init.host + filmUrl; + + if (isfilm) + { + res = new List() { new Uaflix.Models.EpisodeLinkInfo() { url = filmUrl } }; + _hybridCache.Set(memKey, res, cacheTime(20)); + return res; + } + + var filmHtml = await Http.Get(filmUrl, headers: headers, proxy: _proxyManager.Get()); + doc.LoadHtml(filmHtml); + + res = new List(); + var episodeNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'frels2')]//a[contains(@class, 'vi-img')]"); + if (episodeNodes != null) + { + foreach (var episodeNode in episodeNodes.Reverse().ToList()) + { + 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) + { + res.Add(new Uaflix.Models.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) + }); + } + } + } + + if (res.Count == 0) + { + var iframe = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'video-box')]//iframe[contains(@src, 'ashdi.vip/serial/')]"); + if (iframe != null) + { + res.Add(new Uaflix.Models.EpisodeLinkInfo() { url = filmUrl, season = 1, episode = 1 }); + } + } + + 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 ParseEpisode(string url) + { + var result = new Uaflix.Models.PlayResult() { streams = new List<(string, string)>() }; + try + { + 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[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; + } + + if (iframeUrl.Contains("zetvideo.net")) + result.streams = await ParseAllZetvideoSources(iframeUrl); + else if (iframeUrl.Contains("ashdi.vip")) + { + 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}"); + } + return result; + } + + async Task> ParseAllZetvideoSources(string iframeUrl) + { + var result = new List<(string link, string quality)>(); + 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)>(); + 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) + { + var html = await Http.Get($"https://ashdi.vip/vod/{id}", 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); + } + } +} \ No newline at end of file diff --git a/Uaflix/manifest.json b/Uaflix/manifest.json index cb7601b..87d6ce9 100644 --- a/Uaflix/manifest.json +++ b/Uaflix/manifest.json @@ -1,6 +1,6 @@ { "enable": true, - "version": 1, + "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 index 2d2698e..4ad341c 100644 --- a/Unimay/Controllers/Controller.cs +++ b/Unimay/Controllers/Controller.cs @@ -29,65 +29,35 @@ namespace Unimay.Controllers if (await IsBadInitialization(init, rch: false)) return badInitMsg; - var proxy = proxyManager.Get(); + var invoke = new UnimayInvoke(init, hybridCache, OnLog, proxyManager); if (!string.IsNullOrEmpty(code)) { // Fetch release details - return await Release(init, proxy, code, title, original_title, serial, s, e, play, rjson); + return await Release(invoke, init, code, title, original_title, serial, s, e, play, rjson); } else { // Search - return await Search(init, proxy, title, original_title, serial, rjson); + return await Search(invoke, init, title, original_title, serial, rjson); } } - async ValueTask Search(OnlinesSettings init, System.Net.WebProxy proxy, string title, string original_title, int serial, bool 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 () => { - if (!hybridCache.TryGetValue(memKey, out JArray searchResults)) - { - string searchQuery = HttpUtility.UrlEncode(title ?? original_title ?? ""); - string searchUrl = $"{init.host}/release/search?page=0&page_size=10&title={searchQuery}"; - - var headers = httpHeaders(init); - JObject root = await Http.Get(searchUrl, timeoutSeconds: 8, proxy: proxy, headers: headers); - - if (root == null || !root.ContainsKey("content") || ((JArray)root["content"]).Count == 0) - { - proxyManager.Refresh(); - return OnError("search failed"); - } - - searchResults = (JArray)root["content"]; - hybridCache.Set(memKey, searchResults, cacheTime(30, init: init)); - } - - if (searchResults == null || searchResults.Count == 0) + 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.Count); + var stpl = new SimilarTpl(searchResults.Content.Count); + var results = invoke.GetSearchResults(host, searchResults, title, original_title, serial); - foreach (JObject item in searchResults) + foreach (var (itemTitle, itemYear, itemType, releaseUrl) in results) { - string itemCode = item.Value("code"); - string itemTitle = item["names"]?["ukr"]?.Value() ?? item.Value("title"); - string itemYear = item.Value("year"); - string itemType = item.Value("type"); // "Телесеріал" or "Фільм" - - // Filter by serial if specified (0: movie "Фільм", 1: serial "Телесеріал") - if (serial != -1) - { - bool isMovie = itemType == "Фільм"; - if ((serial == 0 && !isMovie) || (serial == 1 && isMovie)) - continue; - } - - string releaseUrl = $"{host}/unimay?code={itemCode}&title={HttpUtility.UrlEncode(itemTitle)}&original_title={HttpUtility.UrlEncode(original_title ?? "")}&serial={serial}"; stpl.Append(itemTitle, itemYear, itemType, releaseUrl); } @@ -95,34 +65,18 @@ namespace Unimay.Controllers }); } - async ValueTask Release(OnlinesSettings init, System.Net.WebProxy proxy, string code, string title, string original_title, int serial, int s, int e, bool play, bool rjson) + 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 () => { - if (!hybridCache.TryGetValue(memKey, out JObject releaseDetail)) - { - string releaseUrl = $"{init.host}/release?code={code}"; - - var headers = httpHeaders(init); - JObject root = await Http.Get(releaseUrl, timeoutSeconds: 8, proxy: proxy, headers: headers); - - if (root == null) - { - proxyManager.Refresh(); - return OnError("release failed"); - } - - releaseDetail = root; - hybridCache.Set(memKey, releaseDetail, cacheTime(60, init: init)); - } - + var releaseDetail = await invoke.Release(code); if (releaseDetail == null) return OnError("no release detail"); - string itemType = releaseDetail.Value("type"); - JArray playlist = (JArray)releaseDetail["playlist"]; + string itemType = releaseDetail.Type; + var playlist = releaseDetail.Playlist; if (playlist == null || playlist.Count == 0) return OnError("no playlist"); @@ -130,33 +84,32 @@ namespace Unimay.Controllers if (play) { // Get specific episode - JObject episode = null; + Unimay.Models.Episode episode = null; if (itemType == "Телесеріал") { if (s <= 0 || e <= 0) return OnError("invalid episode"); - episode = playlist.FirstOrDefault(ep => (int?)ep["number"] == e) as JObject; + episode = playlist.FirstOrDefault(ep => ep.Number == e); } else // Movie { - episode = playlist[0] as JObject; + episode = playlist[0]; } if (episode == null) return OnError("episode not found"); - string masterUrl = episode["hls"]?["master"]?.Value(); + string masterUrl = invoke.GetStreamUrl(episode); if (string.IsNullOrEmpty(masterUrl)) return OnError("no stream"); - return Redirect(HostStreamProxy(init, masterUrl, proxy: proxy)); + return Redirect(HostStreamProxy(init, masterUrl, proxy: proxyManager.Get())); } if (itemType == "Фільм") { - JObject movieEpisode = playlist[0] as JObject; - string movieLink = $"{host}/unimay?code={code}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&serial=0&play=true"; + var (movieTitle, movieLink) = invoke.GetMovieResult(host, releaseDetail, title, original_title); var mtpl = new MovieTpl(title, original_title, 1); - mtpl.Append(movieEpisode["title"]?.Value() ?? title, movieLink); + mtpl.Append(movieTitle, movieLink); return ContentTo(rjson ? mtpl.ToJson() : mtpl.ToHtml()); } else if (itemType == "Телесеріал") @@ -164,27 +117,18 @@ namespace Unimay.Controllers if (s == -1) { // Assume single season + var (seasonName, seasonUrl, seasonId) = invoke.GetSeasonInfo(host, code, title, original_title); var stpl = new SeasonTpl(); - stpl.Append("Сезон 1", $"{host}/unimay?code={code}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&serial=1&s=1", "1"); + stpl.Append(seasonName, seasonUrl, seasonId); return ContentTo(rjson ? stpl.ToJson() : stpl.ToHtml()); } else { // Episodes for season 1 - var episodes = new List(); - foreach (JObject ep in playlist) - { - int epNum = (int)ep["number"]; - if (epNum >= 1 && epNum <= 24) // Assume season 1 - episodes.Add(ep); - } - + var episodes = invoke.GetEpisodesForSeason(host, releaseDetail, title, original_title); var mtpl = new MovieTpl(title, original_title, episodes.Count); - foreach (JObject ep in episodes.OrderBy(ep => (int)ep["number"])) + foreach (var (epTitle, epLink) in episodes) { - int epNum = (int)ep["number"]; - string epTitle = ep["title"]?.Value() ?? $"Епізод {epNum}"; - string epLink = $"{host}/unimay?code={code}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&serial=1&s=1&e={epNum}&play=true"; mtpl.Append(epTitle, epLink); } return ContentTo(rjson ? mtpl.ToJson() : mtpl.ToHtml()); diff --git a/Unimay/ModInit.cs b/Unimay/ModInit.cs index 872538f..fa65c33 100644 --- a/Unimay/ModInit.cs +++ b/Unimay/ModInit.cs @@ -1,5 +1,6 @@ using Shared; using Shared.Models.Online.Settings; +using Shared.Models.Module; namespace Unimay { @@ -10,9 +11,9 @@ namespace Unimay /// /// модуль загружен /// - public static void loaded() + public static void loaded(InitspaceModel initspace) { - Unimay = new OnlinesSettings("Unimay", "https://api.unimay.media/v1", streamproxy: true) + Unimay = new OnlinesSettings("Unimay", "https://api.unimay.media/v1", streamproxy: false) { displayname = "Unimay" }; diff --git a/Unimay/Models/Episode.cs b/Unimay/Models/Episode.cs new file mode 100644 index 0000000..f837ebf --- /dev/null +++ b/Unimay/Models/Episode.cs @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..c9e6bd4 --- /dev/null +++ b/Unimay/Models/ReleaseResponse.cs @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..615278c --- /dev/null +++ b/Unimay/Models/SearchResponse.cs @@ -0,0 +1,41 @@ +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/Unimay.csproj b/Unimay/Unimay.csproj index 6c529d1..9ced83e 100644 --- a/Unimay/Unimay.csproj +++ b/Unimay/Unimay.csproj @@ -9,7 +9,9 @@ - + + ..\..\Shared.dll + \ No newline at end of file diff --git a/Unimay/UnimayInvoke.cs b/Unimay/UnimayInvoke.cs new file mode 100644 index 0000000..2d9ad82 --- /dev/null +++ b/Unimay/UnimayInvoke.cs @@ -0,0 +1,173 @@ +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; + +namespace Unimay +{ + public class UnimayInvoke + { + 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}"; + + 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}"; + + 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") + }; + } + 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); + } + } +} \ No newline at end of file diff --git a/Unimay/manifest.json b/Unimay/manifest.json index f9646f2..c701751 100644 --- a/Unimay/manifest.json +++ b/Unimay/manifest.json @@ -1,6 +1,6 @@ { "enable": true, - "version": 1, + "version": 2, "initspace": "Unimay.ModInit", "online": "Unimay.OnlineApi" } \ No newline at end of file