using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Shared; using Shared.Engine; using Shared.Models; using Shared.Models.Online.Settings; using Makhno.Models; namespace Makhno { public class MakhnoInvoke { private const string WormholeHost = "http://wormhole.lampame.v6.rocks/"; private const string AshdiHost = "https://ashdi.vip"; 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 readonly OnlinesSettings _init; private readonly IHybridCache _hybridCache; private readonly Action _onLog; private readonly ProxyManager _proxyManager; public MakhnoInvoke(OnlinesSettings init, IHybridCache hybridCache, Action onLog, ProxyManager proxyManager) { _init = init; _hybridCache = hybridCache; _onLog = onLog; _proxyManager = proxyManager; } public async Task GetWormholePlay(string imdbId) { if (string.IsNullOrWhiteSpace(imdbId)) return null; string url = $"{WormholeHost}?imdb_id={imdbId}"; try { var headers = new List() { new HeadersModel("User-Agent", Http.UserAgent) }; string response = await Http.Get(_init.cors(url), timeoutSeconds: 4, headers: headers, proxy: _proxyManager.Get()); if (string.IsNullOrWhiteSpace(response)) return null; var payload = JsonConvert.DeserializeObject(response); return string.IsNullOrWhiteSpace(payload?.play) ? null : payload.play; } catch (Exception ex) { _onLog($"Makhno wormhole error: {ex.Message}"); return null; } } public async Task> SearchUaTUT(string query, string imdbId = null) { try { string searchUrl = $"{_init.apihost}/search.php"; 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($"Makhno UaTUT search error: {ex.Message}"); return new List(); } } private async Task> PerformSearch(string searchUrl, string query) { string url = $"{searchUrl}?q={WebUtility.UrlEncode(query)}"; _onLog($"Makhno UaTUT searching: {url}"); var headers = new List() { new HeadersModel("User-Agent", Http.UserAgent) }; 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($"Makhno UaTUT found {results?.Count ?? 0} results for query: {query}"); return results; } catch (Exception ex) { _onLog($"Makhno UaTUT parse error: {ex.Message}"); return null; } } public async Task GetMoviePageContent(string movieId) { try { string url = $"{_init.apihost}/{movieId}"; _onLog($"Makhno UaTUT getting movie page: {url}"); var headers = new List() { new HeadersModel("User-Agent", Http.UserAgent) }; var response = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get()); return response; } catch (Exception ex) { _onLog($"Makhno UaTUT GetMoviePageContent error: {ex.Message}"); return null; } } public string GetPlayerUrl(string moviePageContent) { try { if (string.IsNullOrEmpty(moviePageContent)) return null; var match = Regex.Match(moviePageContent, @"]*id=[""']vip-player[""'][^>]*src=[""']([^""']+)", RegexOptions.IgnoreCase); if (match.Success) return NormalizePlayerUrl(match.Groups[1].Value); match = Regex.Match(moviePageContent, @"]*id=[""']alt-player[""'][^>]*src=[""']([^""']+)", RegexOptions.IgnoreCase); if (match.Success) return NormalizePlayerUrl(match.Groups[1].Value); var iframeMatches = Regex.Matches(moviePageContent, @"]*(?:id=[""']([^""']+)[""'])?[^>]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase); foreach (Match iframe in iframeMatches) { string iframeId = iframe.Groups[1].Value?.ToLowerInvariant(); string src = iframe.Groups[2].Value; if (string.IsNullOrEmpty(src)) continue; if (!string.IsNullOrEmpty(iframeId) && iframeId.Contains("player")) return NormalizePlayerUrl(src); if (src.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase) || src.Contains("zetvideo.net", StringComparison.OrdinalIgnoreCase) || src.Contains("player", StringComparison.OrdinalIgnoreCase)) return NormalizePlayerUrl(src); } var urlMatch = Regex.Match(moviePageContent, @"(https?://[^""'\s>]+/(?:vod|serial)/\d+[^""'\s>]*)", RegexOptions.IgnoreCase); if (urlMatch.Success) return NormalizePlayerUrl(urlMatch.Groups[1].Value); return null; } catch (Exception ex) { _onLog($"Makhno UaTUT GetPlayerUrl error: {ex.Message}"); return null; } } private string NormalizePlayerUrl(string src) { if (string.IsNullOrEmpty(src)) return null; if (src.StartsWith("//")) return $"https:{src}"; return src; } public async Task GetPlayerData(string playerUrl) { if (string.IsNullOrEmpty(playerUrl)) return null; try { string sourceUrl = WithAshdiMultivoice(playerUrl); string requestUrl = sourceUrl; var headers = new List() { new HeadersModel("User-Agent", Http.UserAgent) }; if (sourceUrl.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase)) { headers.Add(new HeadersModel("Referer", "https://ashdi.vip/")); } if (ApnHelper.IsAshdiUrl(sourceUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost)) requestUrl = ApnHelper.WrapUrl(_init, sourceUrl); _onLog($"Makhno getting player data from: {requestUrl}"); 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($"Makhno GetPlayerData error: {ex.Message}"); return null; } } private PlayerData ParsePlayerData(string html) { try { if (string.IsNullOrEmpty(html)) return null; var fileMatch = Regex.Match(html, @"file:'([^']+)'", RegexOptions.IgnoreCase); if (!fileMatch.Success) fileMatch = Regex.Match(html, @"file:\s*""([^""]+)""", RegexOptions.IgnoreCase); if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("[")) { string file = fileMatch.Groups[1].Value; var posterMatch = Regex.Match(html, @"poster:[""']([^""']+)[""']", RegexOptions.IgnoreCase); return new PlayerData { File = file, Poster = posterMatch.Success ? posterMatch.Groups[1].Value : null, Voices = new List(), Movies = new List() { new MovieVariant { File = file, Quality = DetectQualityTag(file) ?? "auto", Title = BuildMovieTitle("Основне джерело", file, 1) } } }; } string jsonData = ExtractPlayerJson(html); if (jsonData == null) _onLog("Makhno ParsePlayerData: file array not found"); else _onLog($"Makhno ParsePlayerData: file array length={jsonData.Length}"); if (!string.IsNullOrEmpty(jsonData)) { var voices = ParseVoicesJson(jsonData); var movies = ParseMovieVariantsJson(jsonData); _onLog($"Makhno ParsePlayerData: voices={voices?.Count ?? 0}"); return new PlayerData { File = movies.FirstOrDefault()?.File, Poster = null, Voices = voices, Movies = movies }; } var m3u8Match = Regex.Match(html, @"(https?://[^""'\s>]+\.m3u8[^""'\s>]*)", RegexOptions.IgnoreCase); if (m3u8Match.Success) { _onLog("Makhno ParsePlayerData: fallback m3u8 match"); return new PlayerData { File = m3u8Match.Groups[1].Value, Poster = null, Voices = new List(), Movies = new List() { new MovieVariant { File = m3u8Match.Groups[1].Value, Quality = DetectQualityTag(m3u8Match.Groups[1].Value) ?? "auto", Title = BuildMovieTitle("Основне джерело", m3u8Match.Groups[1].Value, 1) } } }; } var sourceMatch = Regex.Match(html, @"]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase); if (sourceMatch.Success) { _onLog("Makhno ParsePlayerData: fallback source match"); return new PlayerData { File = sourceMatch.Groups[1].Value, Poster = null, Voices = new List(), Movies = new List() { new MovieVariant { File = sourceMatch.Groups[1].Value, Quality = DetectQualityTag(sourceMatch.Groups[1].Value) ?? "auto", Title = BuildMovieTitle("Основне джерело", sourceMatch.Groups[1].Value, 1) } } }; } return null; } catch (Exception ex) { _onLog($"Makhno ParsePlayerData error: {ex.Message}"); return null; } } private List ParseVoicesJson(string jsonData) { try { var voicesArray = JsonConvert.DeserializeObject>(jsonData); var voices = new List(); if (voicesArray == null) return voices; foreach (var voiceGroup in voicesArray) { var voice = new Voice { Name = voiceGroup["title"]?.ToString(), Seasons = new List() }; var seasons = voiceGroup["folder"] as JArray; if (seasons != null) { foreach (var seasonGroup in seasons) { string seasonTitle = seasonGroup["title"]?.ToString() ?? string.Empty; var episodes = new List(); var episodesArray = seasonGroup["folder"] as JArray; if (episodesArray != null) { foreach (var episode in episodesArray) { episodes.Add(new Episode { Id = episode["id"]?.ToString(), Title = episode["title"]?.ToString(), File = episode["file"]?.ToString(), Poster = episode["poster"]?.ToString(), Subtitle = episode["subtitle"]?.ToString() }); } } episodes = episodes .OrderBy(item => ExtractEpisodeNumber(item.Title) is null) .ThenBy(item => ExtractEpisodeNumber(item.Title) ?? 0) .ToList(); voice.Seasons.Add(new Season { Title = seasonTitle, Episodes = episodes }); } } voices.Add(voice); } return voices; } catch (Exception ex) { _onLog($"Makhno ParseVoicesJson error: {ex.Message}"); return new List(); } } private List ParseMovieVariantsJson(string jsonData) { try { var voicesArray = JsonConvert.DeserializeObject>(jsonData); var movies = new List(); if (voicesArray == null || voicesArray.Count == 0) return movies; int index = 1; foreach (var item in voicesArray) { 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($"Makhno ParseMovieVariantsJson error: {ex.Message}"); return new List(); } } private string ExtractPlayerJson(string html) { if (string.IsNullOrEmpty(html)) return null; var startIndex = FindFileArrayStart(html); if (startIndex < 0) return null; string jsonArray = ExtractBracketArray(html, startIndex); if (string.IsNullOrEmpty(jsonArray)) return null; return jsonArray .Replace("\\'", "'") .Replace("\\\"", "\""); } private int FindFileArrayStart(string html) { int playerStart = html.IndexOf("Playerjs", StringComparison.OrdinalIgnoreCase); if (playerStart >= 0) { int playerIndex = FindFileArrayStartInRange(html, playerStart); if (playerIndex >= 0) return playerIndex; } int index = FindFileArrayIndex(html, "file:'["); if (index >= 0) return index; index = FindFileArrayIndex(html, "file:\"["); if (index >= 0) return index; var match = Regex.Match(html, @"file\s*:\s*'?\[", RegexOptions.IgnoreCase); if (match.Success) return match.Index + match.Value.LastIndexOf('['); return -1; } private int FindFileArrayStartInRange(string html, int startIndex) { int searchStart = startIndex; int searchEnd = Math.Min(html.Length, startIndex + 200000); int tokenIndex = IndexOfIgnoreCase(html, "file:'[", searchStart, searchEnd); if (tokenIndex >= 0) return html.IndexOf('[', tokenIndex); tokenIndex = IndexOfIgnoreCase(html, "file:\"[", searchStart, searchEnd); if (tokenIndex >= 0) return html.IndexOf('[', tokenIndex); tokenIndex = IndexOfIgnoreCase(html, "file", searchStart, searchEnd); if (tokenIndex >= 0) { int bracketIndex = html.IndexOf('[', tokenIndex); if (bracketIndex >= 0 && bracketIndex < searchEnd) return bracketIndex; } return -1; } private int IndexOfIgnoreCase(string text, string value, int startIndex, int endIndex) { int index = text.IndexOf(value, startIndex, StringComparison.OrdinalIgnoreCase); if (index >= 0 && index < endIndex) return index; return -1; } private int FindFileArrayIndex(string html, string token) { int tokenIndex = html.IndexOf(token, StringComparison.OrdinalIgnoreCase); if (tokenIndex < 0) return -1; int bracketIndex = html.IndexOf('[', tokenIndex); return bracketIndex; } private string ExtractBracketArray(string text, int startIndex) { if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[') return null; int depth = 0; bool inString = false; bool escape = false; char quoteChar = '\0'; for (int i = startIndex; i < text.Length; i++) { char ch = text[i]; if (inString) { if (escape) { escape = false; continue; } if (ch == '\\') { escape = 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; } private int? ExtractEpisodeNumber(string value) { if (string.IsNullOrEmpty(value)) return null; var match = Regex.Match(value, @"(\d+)"); if (!match.Success) return null; if (int.TryParse(match.Groups[1].Value, out int num)) return num; return null; } 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; } public string ExtractAshdiPath(string value) { if (string.IsNullOrWhiteSpace(value)) return null; var match = Regex.Match(value, @"https?://(?:www\.)?ashdi\.vip/((?:vod|serial)/\d+)", RegexOptions.IgnoreCase); if (match.Success) return match.Groups[1].Value; match = Regex.Match(value, @"\b((?:vod|serial)/\d+)\b", RegexOptions.IgnoreCase); if (match.Success) return match.Groups[1].Value; return null; } public string BuildAshdiUrl(string path) { if (string.IsNullOrWhiteSpace(path)) return null; if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return path; return $"{AshdiHost}/{path.TrimStart('/')}"; } public async Task GetAshdiPath(string movieId) { if (string.IsNullOrWhiteSpace(movieId)) return null; var page = await GetMoviePageContent(movieId); if (string.IsNullOrWhiteSpace(page)) return null; var playerUrl = GetPlayerUrl(page); var path = ExtractAshdiPath(playerUrl); if (!string.IsNullOrWhiteSpace(path)) return path; return ExtractAshdiPath(page); } public SearchResult SelectUaTUTItem(List items, string imdbId, int? year, string title, string titleEn) { if (items == null || items.Count == 0) return null; var candidates = items.Where(item => ImdbMatch(item, imdbId) && YearMatch(item, year)).ToList(); if (candidates.Count == 1) return candidates[0]; if (candidates.Count > 1) return null; candidates = items.Where(item => ImdbMatch(item, imdbId) && TitleMatch(item, title, titleEn)).ToList(); if (candidates.Count == 1) return candidates[0]; if (candidates.Count > 1) return null; candidates = items.Where(item => YearMatch(item, year) && TitleMatch(item, title, titleEn)).ToList(); if (candidates.Count == 1) return candidates[0]; return null; } private bool ImdbMatch(SearchResult item, string imdbId) { if (string.IsNullOrWhiteSpace(imdbId) || item == null) return false; return string.Equals(item.ImdbId?.Trim(), imdbId.Trim(), StringComparison.OrdinalIgnoreCase); } private bool YearMatch(SearchResult item, int? year) { if (year == null || item == null) return false; var itemYear = YearInt(item.Year); return itemYear.HasValue && itemYear.Value == year.Value; } private bool TitleMatch(SearchResult item, string title, string titleEn) { if (item == null) return false; string itemTitle = NormalizeTitle(item.Title); string itemTitleEn = NormalizeTitle(item.TitleEn); string targetTitle = NormalizeTitle(title); string targetTitleEn = NormalizeTitle(titleEn); return (itemTitle.Length > 0 && targetTitle.Length > 0 && itemTitle == targetTitle) || (itemTitle.Length > 0 && targetTitleEn.Length > 0 && itemTitle == targetTitleEn) || (itemTitleEn.Length > 0 && targetTitle.Length > 0 && itemTitleEn == targetTitle) || (itemTitleEn.Length > 0 && targetTitleEn.Length > 0 && itemTitleEn == targetTitleEn); } private string NormalizeTitle(string value) { if (string.IsNullOrWhiteSpace(value)) return string.Empty; string text = value.ToLowerInvariant(); text = Regex.Replace(text, @"[^\w\s]+", " "); text = Regex.Replace(text, @"\b(season|сезон|частина|part|ova|special|movie|film)\b", " "); text = Regex.Replace(text, @"\b(\d+)(st|nd|rd|th)\b", "$1"); text = Regex.Replace(text, @"\b\d+\b", " "); text = Regex.Replace(text, @"\s+", " "); return text.Trim(); } private int? YearInt(string value) { if (string.IsNullOrWhiteSpace(value)) return null; if (int.TryParse(value.Trim(), out int result)) return result; return null; } public async Task<(JObject item, string mediaType)?> FetchTmdbByImdb(string imdbId, int? year) { if (string.IsNullOrWhiteSpace(imdbId)) return null; try { string apiKey = AppInit.conf?.tmdb?.api_key; if (string.IsNullOrWhiteSpace(apiKey)) return null; string tmdbUrl = $"http://{AppInit.conf.listen.localhost}:{AppInit.conf.listen.port}/tmdb/api/3/find/{imdbId}?external_source=imdb_id&api_key={apiKey}&language=en-US"; var headers = new List() { new HeadersModel("User-Agent", Http.UserAgent) }; JObject payload = await Http.Get(tmdbUrl, timeoutSeconds: 6, headers: headers); if (payload == null) return null; var movieResults = payload["movie_results"] as JArray ?? new JArray(); var tvResults = payload["tv_results"] as JArray ?? new JArray(); var candidates = new List<(JObject item, string mediaType)>(); foreach (var item in movieResults.OfType()) candidates.Add((item, "movie")); foreach (var item in tvResults.OfType()) candidates.Add((item, "tv")); if (candidates.Count == 0) return null; if (year.HasValue) { string yearText = year.Value.ToString(); foreach (var candidate in candidates) { string dateValue = candidate.mediaType == "movie" ? candidate.item.Value("release_date") : candidate.item.Value("first_air_date"); if (!string.IsNullOrWhiteSpace(dateValue) && dateValue.StartsWith(yearText, StringComparison.Ordinal)) return candidate; } } return candidates[0]; } catch (Exception ex) { _onLog($"Makhno TMDB fetch failed: {ex.Message}"); return null; } } public async Task PostWormholeAsync(object payload) { try { var headers = new List() { new HeadersModel("Content-Type", "application/json"), new HeadersModel("User-Agent", Http.UserAgent) }; string json = JsonConvert.SerializeObject(payload, Formatting.None); await Http.Post(_init.cors(WormholeHost), json, timeoutSeconds: 6, headers: headers, proxy: _proxyManager.Get()); return true; } catch (Exception ex) { _onLog($"Makhno wormhole insert failed: {ex.Message}"); return false; } } private class WormholeResponse { public string play { get; set; } } } }