diff --git a/UaTUT/Controller.cs b/UaTUT/Controller.cs new file mode 100644 index 0000000..4aac662 --- /dev/null +++ b/UaTUT/Controller.cs @@ -0,0 +1,424 @@ +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 new file mode 100644 index 0000000..25a6e13 --- /dev/null +++ b/UaTUT/ModInit.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using Shared; +using Shared.Engine; +using Newtonsoft.Json.Linq; +using Shared.Models.Online.Settings; +using Shared.Models.Module; + +namespace 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 new file mode 100644 index 0000000..c9ea23d --- /dev/null +++ b/UaTUT/Models/UaTUTModels.cs @@ -0,0 +1,61 @@ +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 new file mode 100644 index 0000000..5a5640b --- /dev/null +++ b/UaTUT/OnlineApi.cs @@ -0,0 +1,25 @@ +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; + 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 new file mode 100644 index 0000000..1fbe365 --- /dev/null +++ b/UaTUT/UaTUT.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + library + true + + + + + ..\..\Shared.dll + + + + diff --git a/UaTUT/UaTUTInvoke.cs b/UaTUT/UaTUTInvoke.cs new file mode 100644 index 0000000..8a80983 --- /dev/null +++ b/UaTUT/UaTUTInvoke.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +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 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}"); + + 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}"); + + 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}"); + + 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 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 new file mode 100644 index 0000000..79200f2 --- /dev/null +++ b/UaTUT/manifest.json @@ -0,0 +1,7 @@ +{ + "enable": true, + "version": 2, + "initspace": "UaTUT.ModInit", + "online": "UaTUT.OnlineApi" +} +