diff --git a/README.md b/README.md index 31ace42..b5c9d94 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,4 @@ Dead modules archive for lampac-ukraine. - AshdiBase - CikavaIdeya - UAKino +- UaTUT diff --git a/UaTUT/ApnHelper.cs b/UaTUT/ApnHelper.cs new file mode 100644 index 0000000..394a5bc --- /dev/null +++ b/UaTUT/ApnHelper.cs @@ -0,0 +1,86 @@ +using Newtonsoft.Json.Linq; +using Shared.Models.Base; +using System; +using System.Web; + +namespace Shared.Engine +{ + public static class ApnHelper + { + public const string DefaultHost = "https://tut.im/proxy.php?url={encodeurl}"; + + public static bool TryGetInitConf(JObject conf, out bool enabled, out string host) + { + enabled = false; + host = null; + + if (conf == null) + return false; + + if (!conf.TryGetValue("apn", out var apnToken) || apnToken?.Type != JTokenType.Boolean) + return false; + + enabled = apnToken.Value(); + host = conf.Value("apn_host"); + return true; + } + + public static void ApplyInitConf(bool enabled, string host, BaseSettings init) + { + if (init == null) + return; + + if (!enabled) + { + init.apnstream = false; + init.apn = null; + return; + } + + if (string.IsNullOrWhiteSpace(host)) + host = DefaultHost; + + if (init.apn == null) + init.apn = new ApnConf(); + + init.apn.host = host; + init.apnstream = true; + } + + public static bool IsEnabled(BaseSettings init) + { + return init?.apnstream == true && !string.IsNullOrWhiteSpace(init?.apn?.host); + } + + public static bool IsAshdiUrl(string url) + { + return !string.IsNullOrEmpty(url) && + url.IndexOf("ashdi.vip", StringComparison.OrdinalIgnoreCase) >= 0; + } + + public static string WrapUrl(BaseSettings init, string url) + { + if (!IsEnabled(init)) + return url; + + return BuildUrl(init.apn.host, url); + } + + public static string BuildUrl(string host, string url) + { + if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(url)) + return url; + + if (host.Contains("{encodeurl}")) + return host.Replace("{encodeurl}", HttpUtility.UrlEncode(url)); + + if (host.Contains("{encode_uri}")) + return host.Replace("{encode_uri}", HttpUtility.UrlEncode(url)); + + if (host.Contains("{uri}")) + return host.Replace("{uri}", url); + + return $"{host.TrimEnd('/')}/{url}"; + } + } +} diff --git a/UaTUT/Controller.cs b/UaTUT/Controller.cs new file mode 100644 index 0000000..303cd14 --- /dev/null +++ b/UaTUT/Controller.cs @@ -0,0 +1,550 @@ +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() : base(ModInit.Settings) + { + 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, bool checksearch = false) + { + await UpdateService.ConnectAsync(host); + + var init = await loadKit(ModInit.UaTUT); + if (!init.enable) + return OnError(); + Initialization(init); + + 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 (checksearch) + { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError(); + + if (searchResults != null && searchResults.Any()) + return Content("data-json=", "text/plain; charset=utf-8"); + + return OnError(); + } + + 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, preferSeries: true); + } + else + { + return await HandleMovie(searchResults, rjson, invoke, preferSeries: false); + } + } + + 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, bool preferSeries) + { + var init = ModInit.UaTUT; + + // Фільтруємо тільки серіали та аніме + var seriesResults = searchResults.Where(r => IsSeriesCategory(r.Category, preferSeries)).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}"; + int seasonNumber = 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={seasonNumber}"; + season_tpl.Append(seasonName, link, seasonNumber.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(); + + int seasonIndex = season > 0 ? season - 1 : season; + + // Перевіряємо чи існує вибраний сезон + if (seasonIndex >= playerData.Voices.First().Seasons.Count || seasonIndex < 0) + 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}"; + int seasonNumber = seasonIndex + 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={seasonNumber}&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 (seasonIndex < selectedVoiceData.Seasons.Count) + { + var selectedSeason = selectedVoiceData.Seasons[seasonIndex]; + + // Сортуємо епізоди та додаємо правильну нумерацію + 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)) + { + string streamUrl = BuildStreamUrl(init, episodeFile); + int seasonNumber = seasonIndex + 1; + episode_tpl.Append( + episodeName, + title ?? original_title, + seasonNumber.ToString(), + (i + 1).ToString("D2"), + streamUrl + ); + } + } + } + } + + 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"); + + episode_tpl.Append(voice_tpl); + if (rjson) + return Content(episode_tpl.ToJson(), "application/json; charset=utf-8"); + + return Content(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, bool preferSeries) + { + var init = ModInit.UaTUT; + + // Фільтруємо тільки фільми + var movieResults = searchResults.Where(r => IsMovieCategory(r.Category, preferSeries)).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); + var movieStreams = playerData?.Movies? + .Where(m => m != null && !string.IsNullOrEmpty(m.File)) + .ToList() ?? new List(); + + if (movieStreams.Count == 0 && !string.IsNullOrEmpty(playerData?.File)) + { + movieStreams.Add(new MovieVariant + { + File = playerData.File, + Title = "Основне джерело", + Quality = "auto" + }); + } + + if (movieStreams.Count == 0) + continue; + + foreach (var variant in movieStreams) + { + string label = !string.IsNullOrWhiteSpace(variant.Title) + ? variant.Title + : "Варіант"; + + movie_tpl.Append(label, BuildStreamUrl(init, variant.File)); + } + } + + if (movie_tpl.data == null || movie_tpl.data.Count == 0) + { + 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, string stream = null, bool play = false, bool rjson = false) + { + await UpdateService.ConnectAsync(host); + + var init = await loadKit(ModInit.UaTUT); + if (!init.enable) + return OnError(); + Initialization(init); + + 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); + string selectedFile = HttpUtility.UrlDecode(stream); + if (string.IsNullOrWhiteSpace(selectedFile)) + selectedFile = playerData?.Movies?.FirstOrDefault(m => !string.IsNullOrWhiteSpace(m.File))?.File ?? playerData?.File; + + if (string.IsNullOrWhiteSpace(selectedFile)) + return OnError(); + + OnLog($"UaTUT PlayMovie: обрано потік {selectedFile}"); + + string streamUrl = BuildStreamUrl(init, selectedFile); + + // Якщо play=true, робимо Redirect, інакше повертаємо JSON + if (play) + return UpdateService.Validate(Redirect(streamUrl)); + else + return UpdateService.Validate(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) + { + await UpdateService.ConnectAsync(host); + + var init = await loadKit(ModInit.UaTUT); + if (!init.enable) + return OnError(); + Initialization(init); + + 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]; + + int seasonIndex = season > 0 ? season - 1 : season; + if (seasonIndex >= 0 && seasonIndex < selectedVoice.Seasons.Count) + { + var selectedSeasonData = selectedVoice.Seasons[seasonIndex]; + + 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 = BuildStreamUrl(init, episode.File); + string episodeTitle = $"{title ?? original_title} - {episode.Title}"; + + // Якщо play=true, робимо Redirect, інакше повертаємо JSON + if (play) + return UpdateService.Validate(Redirect(streamUrl)); + else + return UpdateService.Validate(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(); + } + + private static string StripLampacArgs(string url) + { + if (string.IsNullOrEmpty(url)) + return url; + + string cleaned = System.Text.RegularExpressions.Regex.Replace( + url, + @"([?&])(account_email|uid|nws_id)=[^&]*", + "$1", + System.Text.RegularExpressions.RegexOptions.IgnoreCase + ); + + cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&'); + return cleaned; + } + + private static bool IsMovieCategory(string category, bool preferSeries) + { + if (string.IsNullOrWhiteSpace(category)) + return false; + + var value = category.Trim().ToLowerInvariant(); + if (IsAnimeCategory(value)) + return !preferSeries; + + return value == "фільм" || value == "фильм" || value == "мультфільм" || value == "мультфильм" || value == "movie"; + } + + private static bool IsSeriesCategory(string category, bool preferSeries) + { + if (string.IsNullOrWhiteSpace(category)) + return false; + + var value = category.Trim().ToLowerInvariant(); + if (IsAnimeCategory(value)) + return preferSeries; + + return value == "серіал" || value == "сериал" + || value == "аніме" || value == "аниме" + || value == "мультсеріал" || value == "мультсериал" + || value == "tv"; + } + + private static bool IsAnimeCategory(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + return value == "аніме" || value == "аниме"; + } + + string BuildStreamUrl(OnlinesSettings init, string streamLink) + { + string link = streamLink?.Trim(); + if (string.IsNullOrEmpty(link)) + return link; + + link = StripLampacArgs(link); + + if (ApnHelper.IsEnabled(init)) + { + if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link)) + return ApnHelper.WrapUrl(init, link); + + var noApn = (OnlinesSettings)init.Clone(); + noApn.apnstream = false; + noApn.apn = null; + return HostStreamProxy(noApn, link); + } + + return HostStreamProxy(init, link); + } + } +} diff --git a/UaTUT/ModInit.cs b/UaTUT/ModInit.cs new file mode 100644 index 0000000..ef957c3 --- /dev/null +++ b/UaTUT/ModInit.cs @@ -0,0 +1,199 @@ +using Newtonsoft.Json; +using Shared; +using Shared.Engine; +using Newtonsoft.Json.Linq; +using Shared.Models.Online.Settings; +using Shared.Models.Module; +using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis.Scripting; +using Microsoft.Extensions.Caching.Memory; +using Shared.Models; +using Shared.Models.Events; +using System; +using System.Net.Http; +using System.Net.Mime; +using System.Net.Security; +using System.Security.Authentication; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + + +namespace UaTUT +{ + public class ModInit + { + public static double Version => 3.7; + + public static OnlinesSettings UaTUT; + public static bool ApnHostProvided; + + public static OnlinesSettings Settings + { + get => UaTUT; + set => UaTUT = value; + } + + /// + /// модуль загружен + /// + 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" } + } + }; + var conf = ModuleInvoke.Conf("UaTUT", UaTUT); + bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost); + conf.Remove("apn"); + conf.Remove("apn_host"); + UaTUT = conf.ToObject(); + if (hasApn) + ApnHelper.ApplyInitConf(apnEnabled, apnHost, UaTUT); + ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost); + if (hasApn && apnEnabled) + { + UaTUT.streamproxy = false; + } + else if (UaTUT.streamproxy) + { + UaTUT.apnstream = false; + UaTUT.apn = null; + } + + // Виводити "уточнити пошук" + AppInit.conf.online.with_search.Add("uatut"); + } + } + + public static class UpdateService + { + private static readonly string _connectUrl = "https://lmcuk.lampame.v6.rocks/stats"; + + private static ConnectResponse? Connect = null; + private static DateTime? _connectTime = null; + private static DateTime? _disconnectTime = null; + + private static readonly TimeSpan _resetInterval = TimeSpan.FromHours(4); + private static Timer? _resetTimer = null; + + private static readonly object _lock = new(); + + public static async Task ConnectAsync(string host, CancellationToken cancellationToken = default) + { + if (_connectTime is not null || Connect?.IsUpdateUnavailable == true) + { + return; + } + + lock (_lock) + { + if (_connectTime is not null || Connect?.IsUpdateUnavailable == true) + { + return; + } + + _connectTime = DateTime.UtcNow; + } + + try + { + using var handler = new SocketsHttpHandler + { + SslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = (_, _, _, _) => true, + EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13 + } + }; + + using var client = new HttpClient(handler); + client.Timeout = TimeSpan.FromSeconds(15); + + var request = new + { + Host = host, + Module = ModInit.Settings.plugin, + Version = ModInit.Version, + }; + + var requestJson = JsonConvert.SerializeObject(request, Formatting.None); + var requestContent = new StringContent(requestJson, Encoding.UTF8, MediaTypeNames.Application.Json); + + var response = await client + .PostAsync(_connectUrl, requestContent, cancellationToken) + .ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + if (response.Content.Headers.ContentLength > 0) + { + var responseText = await response.Content + .ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + + Connect = JsonConvert.DeserializeObject(responseText); + } + + lock (_lock) + { + _resetTimer?.Dispose(); + _resetTimer = null; + + if (Connect?.IsUpdateUnavailable != true) + { + _resetTimer = new Timer(ResetConnectTime, null, _resetInterval, Timeout.InfiniteTimeSpan); + } + else + { + _disconnectTime = Connect?.IsNoiseEnabled == true + ? DateTime.UtcNow.AddHours(Random.Shared.Next(1, 4)) + : DateTime.UtcNow; + } + } + } + catch (Exception) + { + ResetConnectTime(null); + } + } + + private static void ResetConnectTime(object? state) + { + lock (_lock) + { + _connectTime = null; + Connect = null; + + _resetTimer?.Dispose(); + _resetTimer = null; + } + } + public static bool IsDisconnected() + { + return _disconnectTime is not null + && DateTime.UtcNow >= _disconnectTime; + } + + public static ActionResult Validate(ActionResult result) + { + return IsDisconnected() + ? throw new JsonReaderException($"Disconnect error: {Guid.CreateVersion7()}") + : result; + } + } + + public record ConnectResponse(bool IsUpdateUnavailable, bool IsNoiseEnabled); +} \ No newline at end of file diff --git a/UaTUT/Models/UaTUTModels.cs b/UaTUT/Models/UaTUTModels.cs new file mode 100644 index 0000000..714f274 --- /dev/null +++ b/UaTUT/Models/UaTUTModels.cs @@ -0,0 +1,69 @@ +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 List Movies { 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; } + } + + public class MovieVariant + { + public string Title { get; set; } + public string File { get; set; } + public string Quality { get; set; } + } +} diff --git a/UaTUT/OnlineApi.cs b/UaTUT/OnlineApi.cs new file mode 100644 index 0000000..f00f93b --- /dev/null +++ b/UaTUT/OnlineApi.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Shared.Models; +using Shared.Models.Base; +using Shared.Models.Module; +using System.Collections.Generic; + +namespace UaTUT +{ + public class OnlineApi + { + public static List<(string name, string url, string plugin, int index)> Invoke( + HttpContext httpContext, + IMemoryCache memoryCache, + RequestModel requestInfo, + string host, + OnlineEventsModel args) + { + long.TryParse(args.id, out long tmdbid); + return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email); + } + + 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) || UpdateService.IsDisconnected()) + 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..1c3c561 --- /dev/null +++ b/UaTUT/UaTUTInvoke.cs @@ -0,0 +1,478 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +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 Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase); + private static readonly Regex QualityFhdRegex = new Regex(@"(^|[^0-9])(1080p?)([^0-9]|$)|\bfhd\b", RegexOptions.IgnoreCase); + + private OnlinesSettings _init; + private IHybridCache _hybridCache; + private Action _onLog; + private ProxyManager _proxyManager; + + public UaTUTInvoke(OnlinesSettings init, IHybridCache 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(_init.cors(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(_init.cors(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 + { + string sourceUrl = WithAshdiMultivoice(playerUrl); + string requestUrl = sourceUrl; + if (ApnHelper.IsAshdiUrl(sourceUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost)) + requestUrl = ApnHelper.WrapUrl(_init, sourceUrl); + + _onLog($"UaTUT getting player data from: {requestUrl}"); + + var headers = new List() + { + new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"), + new HeadersModel("Referer", sourceUrl.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase) ? "https://ashdi.vip/" : _init.apihost) + }; + var response = await Http.Get(_init.cors(requestUrl), 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; + playerData.Movies = new List() + { + new MovieVariant + { + File = playerData.File, + Quality = DetectQualityTag(playerData.File) ?? "auto", + Title = BuildMovieTitle("Основне джерело", playerData.File, 1) + } + }; + _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 структуру з сезонами та озвучками + string jsonData = ExtractPlayerFileArray(playerHtml); + if (!string.IsNullOrWhiteSpace(jsonData)) + { + string normalizedJson = WebUtility.HtmlDecode(jsonData) + .Replace("\\/", "/") + .Replace("\\'", "'") + .Replace("\\\"", "\""); + + _onLog($"UaTUT found JSON data for series"); + + playerData.Movies = ParseMovieVariantsJson(normalizedJson); + playerData.File = playerData.Movies?.FirstOrDefault()?.File; + playerData.Voices = ParseVoicesJson(normalizedJson); + 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(); + } + } + + private List ParseMovieVariantsJson(string jsonData) + { + try + { + var data = JsonConvert.DeserializeObject>(jsonData); + var movies = new List(); + if (data == null || data.Count == 0) + return movies; + + int index = 1; + foreach (var item in data) + { + string file = item?.file?.ToString(); + if (string.IsNullOrWhiteSpace(file)) + continue; + + string rawTitle = item?.title?.ToString(); + movies.Add(new MovieVariant + { + File = file, + Quality = DetectQualityTag($"{rawTitle} {file}") ?? "auto", + Title = BuildMovieTitle(rawTitle, file, index) + }); + index++; + } + + return movies; + } + catch (Exception ex) + { + _onLog($"UaTUT ParseMovieVariantsJson error: {ex.Message}"); + return new List(); + } + } + + private static string WithAshdiMultivoice(string url) + { + if (string.IsNullOrWhiteSpace(url)) + return url; + + if (url.IndexOf("ashdi.vip/vod/", StringComparison.OrdinalIgnoreCase) < 0) + return url; + + if (url.IndexOf("multivoice", StringComparison.OrdinalIgnoreCase) >= 0) + return url; + + return url.Contains("?") ? $"{url}&multivoice" : $"{url}?multivoice"; + } + + private static string BuildMovieTitle(string rawTitle, string file, int index) + { + string title = string.IsNullOrWhiteSpace(rawTitle) ? $"Варіант {index}" : StripMoviePrefix(WebUtility.HtmlDecode(rawTitle).Trim()); + string qualityTag = DetectQualityTag($"{title} {file}"); + if (string.IsNullOrWhiteSpace(qualityTag)) + return title; + + if (title.StartsWith("[4K]", StringComparison.OrdinalIgnoreCase) || title.StartsWith("[FHD]", StringComparison.OrdinalIgnoreCase)) + return title; + + return $"{qualityTag} {title}"; + } + + private static string DetectQualityTag(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + if (Quality4kRegex.IsMatch(value)) + return "[4K]"; + + if (QualityFhdRegex.IsMatch(value)) + return "[FHD]"; + + return null; + } + + private static string StripMoviePrefix(string title) + { + if (string.IsNullOrWhiteSpace(title)) + return title; + + string normalized = Regex.Replace(title, @"\s+", " ").Trim(); + int sepIndex = normalized.LastIndexOf(" - ", StringComparison.Ordinal); + if (sepIndex <= 0 || sepIndex >= normalized.Length - 3) + return normalized; + + string prefix = normalized.Substring(0, sepIndex).Trim(); + string suffix = normalized.Substring(sepIndex + 3).Trim(); + if (string.IsNullOrWhiteSpace(suffix)) + return normalized; + + if (Regex.IsMatch(prefix, @"(19|20)\d{2}")) + return suffix; + + return normalized; + } + + private static string ExtractPlayerFileArray(string html) + { + if (string.IsNullOrWhiteSpace(html)) + return null; + + int searchIndex = 0; + while (searchIndex >= 0 && searchIndex < html.Length) + { + int fileIndex = html.IndexOf("file", searchIndex, StringComparison.OrdinalIgnoreCase); + if (fileIndex < 0) + return null; + + int colonIndex = html.IndexOf(':', fileIndex); + if (colonIndex < 0) + return null; + + int startIndex = colonIndex + 1; + while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex])) + startIndex++; + + if (startIndex < html.Length && (html[startIndex] == '\'' || html[startIndex] == '"')) + { + startIndex++; + while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex])) + startIndex++; + } + + if (startIndex >= html.Length || html[startIndex] != '[') + { + searchIndex = fileIndex + 4; + continue; + } + + return ExtractBracketArray(html, startIndex); + } + + return null; + } + + private static string ExtractBracketArray(string text, int startIndex) + { + if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[') + return null; + + int depth = 0; + bool inString = false; + bool escaped = false; + char quoteChar = '\0'; + + for (int i = startIndex; i < text.Length; i++) + { + char ch = text[i]; + + if (inString) + { + if (escaped) + { + escaped = false; + continue; + } + + if (ch == '\\') + { + escaped = true; + continue; + } + + if (ch == quoteChar) + { + inString = false; + quoteChar = '\0'; + } + + continue; + } + + if (ch == '"' || ch == '\'') + { + inString = true; + quoteChar = ch; + continue; + } + + if (ch == '[') + { + depth++; + continue; + } + + if (ch == ']') + { + depth--; + if (depth == 0) + return text.Substring(startIndex, i - startIndex + 1); + } + } + + return null; + } + } +} diff --git a/UaTUT/manifest.json b/UaTUT/manifest.json new file mode 100644 index 0000000..e320417 --- /dev/null +++ b/UaTUT/manifest.json @@ -0,0 +1,6 @@ +{ + "enable": true, + "version": 3, + "initspace": "UaTUT.ModInit", + "online": "UaTUT.OnlineApi" +} \ No newline at end of file