From 56eb95be3d6b4f904c97e105aef1576314490383 Mon Sep 17 00:00:00 2001 From: Felix Date: Wed, 25 Feb 2026 16:07:13 +0200 Subject: [PATCH] feat: Integrate KlonFUN service for content resolution and player URL extraction, update Wormhole host, and simplify EnrichWormhole parameters. --- Makhno/Controller.cs | 71 +++++--- Makhno/MakhnoInvoke.cs | 317 +++++++++++++++++++++++++++++++++- Makhno/ModInit.cs | 2 +- Makhno/Models/MakhnoModels.cs | 8 + 4 files changed, 371 insertions(+), 27 deletions(-) diff --git a/Makhno/Controller.cs b/Makhno/Controller.cs index 755087a..6db8980 100644 --- a/Makhno/Controller.cs +++ b/Makhno/Controller.cs @@ -55,7 +55,7 @@ namespace Makhno { try { - await EnrichWormhole(imdb_id, title, original_title, year, resolved, invoke); + await EnrichWormhole(imdb_id, year, resolved, invoke); } catch (Exception ex) { @@ -479,6 +479,47 @@ namespace Makhno } string searchQuery = originalTitle ?? title; + + string klonSearchCacheKey = $"makhno:klonfun:search:{imdbId ?? searchQuery}"; + var klonSearchResults = await InvokeCache>(klonSearchCacheKey, TimeSpan.FromMinutes(10), async () => + { + return await invoke.SearchKlonFUN(imdbId, title, originalTitle); + }); + + if (klonSearchResults != null && klonSearchResults.Count > 0) + { + var klonSelected = invoke.SelectKlonFUNItem(klonSearchResults, year > 0 ? year : null, title, originalTitle); + if (klonSelected != null && !string.IsNullOrWhiteSpace(klonSelected.Url)) + { + string klonPlayerCacheKey = $"makhno:klonfun:player:{klonSelected.Url}"; + string klonPlayerUrl = await InvokeCache(klonPlayerCacheKey, TimeSpan.FromMinutes(10), async () => + { + return await invoke.GetKlonFUNPlayerUrl(klonSelected.Url); + }); + + if (!string.IsNullOrWhiteSpace(klonPlayerUrl)) + { + string klonAshdiPath = invoke.ExtractAshdiPath(klonPlayerUrl); + + return new ResolveResult + { + PlayUrl = klonPlayerUrl, + AshdiPath = klonAshdiPath, + Selected = new SearchResult + { + ImdbId = imdbId, + Title = klonSelected.Title, + TitleEn = originalTitle, + Year = klonSelected.Year > 0 ? klonSelected.Year.ToString() : null, + Category = invoke.IsSerialPlayerUrl(klonPlayerUrl) ? "Серіал" : "Фільм" + }, + IsSerial = serial == 1 || invoke.IsSerialPlayerUrl(klonPlayerUrl) || IsSerialByUrl(klonPlayerUrl, serial), + ShouldEnrich = !string.IsNullOrWhiteSpace(klonAshdiPath) + }; + } + } + } + string searchCacheKey = $"makhno:uatut:search:{imdbId ?? searchQuery}"; var searchResults = await InvokeCache>(searchCacheKey, TimeSpan.FromMinutes(10), async () => @@ -546,43 +587,33 @@ namespace Makhno return url.Contains("/serial/", StringComparison.OrdinalIgnoreCase); } - private async Task EnrichWormhole(string imdbId, string title, string originalTitle, int year, ResolveResult resolved, MakhnoInvoke invoke) + private async Task EnrichWormhole(string imdbId, int year, ResolveResult resolved, MakhnoInvoke invoke) { - if (string.IsNullOrWhiteSpace(imdbId) || resolved?.Selected == null || string.IsNullOrWhiteSpace(resolved.AshdiPath)) + if (string.IsNullOrWhiteSpace(imdbId) || resolved == null || string.IsNullOrWhiteSpace(resolved.AshdiPath)) return; int? yearValue = year > 0 ? year : null; - if (!yearValue.HasValue && int.TryParse(resolved.Selected.Year, out int parsedYear)) + if (!yearValue.HasValue && int.TryParse(resolved.Selected?.Year, out int parsedYear)) yearValue = parsedYear; - var tmdbResult = await invoke.FetchTmdbByImdb(imdbId, yearValue); + var tmdbResult = await invoke.FetchTmdbByImdb(imdbId, yearValue, resolved.IsSerial); if (tmdbResult == null) return; - var (item, mediaType) = tmdbResult.Value; + var (item, _) = tmdbResult.Value; var tmdbId = item.Value("id"); if (!tmdbId.HasValue) return; - string original = item.Value("original_title") - ?? item.Value("original_name") - ?? resolved.Selected.TitleEn - ?? originalTitle - ?? title; - - string resultTitle = resolved.Selected.Title - ?? item.Value("title") - ?? item.Value("name"); + string mediaType = resolved.IsSerial ? "tv" : "movie"; + int serialValue = resolved.IsSerial ? 1 : 0; var payload = new { imdb_id = imdbId, _id = $"{mediaType}:{tmdbId.Value}", - original_title = original, - title = resultTitle, - serial = mediaType == "tv" ? 1 : 0, - ashdi = resolved.AshdiPath, - year = (resolved.Selected.Year ?? yearValue?.ToString()) + serial = serialValue, + ashdi = resolved.AshdiPath }; await invoke.PostWormholeAsync(payload); diff --git a/Makhno/MakhnoInvoke.cs b/Makhno/MakhnoInvoke.cs index 3dd3e89..4b7a614 100644 --- a/Makhno/MakhnoInvoke.cs +++ b/Makhno/MakhnoInvoke.cs @@ -5,6 +5,7 @@ using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using HtmlAgilityPack; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Shared; @@ -17,10 +18,12 @@ namespace Makhno { public class MakhnoInvoke { - private const string WormholeHost = "http://wormhole.lampame.v6.rocks/"; + private const string WormholeHost = "https://wh.lme.isroot.in/"; private const string AshdiHost = "https://ashdi.vip"; + private const string KlonHost = "https://klon.fun"; 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 static readonly Regex YearRegex = new Regex(@"(19|20)\d{2}", RegexOptions.IgnoreCase); private readonly OnlinesSettings _init; private readonly IHybridCache _hybridCache; @@ -62,6 +65,257 @@ namespace Makhno } } + public async Task> SearchKlonFUN(string imdbId, string title, string originalTitle) + { + try + { + if (!string.IsNullOrWhiteSpace(imdbId)) + { + var byImdb = await SearchKlonFUNByQuery(imdbId); + if (byImdb?.Count > 0) + { + _onLog($"Makhno KlonFUN: знайдено {byImdb.Count} результат(ів) за imdb_id={imdbId}"); + return byImdb; + } + } + + var queries = new[] { originalTitle, title } + .Where(q => !string.IsNullOrWhiteSpace(q)) + .Select(q => q.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + var results = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (string query in queries) + { + var partial = await SearchKlonFUNByQuery(query); + if (partial == null || partial.Count == 0) + continue; + + foreach (var item in partial) + { + if (!string.IsNullOrWhiteSpace(item?.Url) && seen.Add(item.Url)) + results.Add(item); + } + + if (results.Count > 0) + break; + } + + if (results.Count > 0) + { + _onLog($"Makhno KlonFUN: знайдено {results.Count} результат(ів) за назвою"); + return results; + } + } + catch (Exception ex) + { + _onLog($"Makhno KlonFUN: помилка пошуку - {ex.Message}"); + } + + return new List(); + } + + public KlonSearchResult SelectKlonFUNItem(List items, int? year, string title, string titleEn) + { + if (items == null || items.Count == 0) + return null; + + if (items.Count == 1) + return items[0]; + + var byYearAndTitle = items + .Where(item => YearMatch(item, year) && TitleMatch(item?.Title, title, titleEn)) + .ToList(); + + if (byYearAndTitle.Count == 1) + return byYearAndTitle[0]; + if (byYearAndTitle.Count > 1) + return null; + + var byTitle = items + .Where(item => TitleMatch(item?.Title, title, titleEn)) + .ToList(); + + if (byTitle.Count == 1) + return byTitle[0]; + if (byTitle.Count > 1) + return null; + + var byYear = items + .Where(item => YearMatch(item, year)) + .ToList(); + + if (byYear.Count == 1) + return byYear[0]; + + return null; + } + + public async Task GetKlonFUNPlayerUrl(string itemUrl) + { + if (string.IsNullOrWhiteSpace(itemUrl)) + return null; + + try + { + var headers = new List() + { + new HeadersModel("User-Agent", Http.UserAgent), + new HeadersModel("Referer", KlonHost) + }; + + string html = await Http.Get(_init.cors(itemUrl), headers: headers, proxy: _proxyManager.Get()); + if (string.IsNullOrWhiteSpace(html)) + return null; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + string playerUrl = doc.DocumentNode + .SelectSingleNode("//div[contains(@class,'film-player')]//iframe") + ?.GetAttributeValue("data-src", null); + + if (string.IsNullOrWhiteSpace(playerUrl)) + { + playerUrl = doc.DocumentNode + .SelectSingleNode("//div[contains(@class,'film-player')]//iframe") + ?.GetAttributeValue("src", null); + } + + if (string.IsNullOrWhiteSpace(playerUrl)) + { + playerUrl = doc.DocumentNode + .SelectSingleNode("//iframe[contains(@src,'ashdi.vip') or contains(@src,'zetvideo.net') or contains(@src,'/vod/') or contains(@src,'/serial/')]") + ?.GetAttributeValue("src", null); + } + + return NormalizeUrl(KlonHost, playerUrl); + } + catch (Exception ex) + { + _onLog($"Makhno KlonFUN: помилка отримання плеєра - {ex.Message}"); + return null; + } + } + + public bool IsSerialPlayerUrl(string playerUrl) + { + return !string.IsNullOrWhiteSpace(playerUrl) + && playerUrl.IndexOf("/serial/", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private async Task> SearchKlonFUNByQuery(string query) + { + if (string.IsNullOrWhiteSpace(query)) + return null; + + try + { + var headers = new List() + { + new HeadersModel("User-Agent", Http.UserAgent), + new HeadersModel("Referer", KlonHost) + }; + + string form = $"do=search&subaction=search&story={WebUtility.UrlEncode(query)}"; + string html = await Http.Post(_init.cors(KlonHost), form, headers: headers, proxy: _proxyManager.Get()); + if (string.IsNullOrWhiteSpace(html)) + return null; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + var results = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + var nodes = doc.DocumentNode.SelectNodes("//div[contains(@class,'short-news__slide-item')]"); + if (nodes != null) + { + foreach (var node in nodes) + { + string href = node.SelectSingleNode(".//a[contains(@class,'short-news__small-card__link')]") + ?.GetAttributeValue("href", null) + ?? node.SelectSingleNode(".//a[contains(@class,'card-link__style')]") + ?.GetAttributeValue("href", null); + + href = NormalizeUrl(KlonHost, href); + if (string.IsNullOrWhiteSpace(href) || !seen.Add(href)) + continue; + + string itemTitle = CleanText(node.SelectSingleNode(".//div[contains(@class,'card-link__text')]")?.InnerText); + if (string.IsNullOrWhiteSpace(itemTitle)) + { + itemTitle = CleanText(node.SelectSingleNode(".//a[contains(@class,'card-link__style')]")?.InnerText); + } + + string poster = node.SelectSingleNode(".//img[contains(@class,'card-poster__img')]") + ?.GetAttributeValue("data-src", null); + if (string.IsNullOrWhiteSpace(poster)) + { + poster = node.SelectSingleNode(".//img[contains(@class,'card-poster__img')]") + ?.GetAttributeValue("src", null); + } + + string meta = CleanText(node.SelectSingleNode(".//div[contains(@class,'subscribe-label-module')]")?.InnerText); + int itemYear = 0; + if (!string.IsNullOrWhiteSpace(meta)) + { + var yearMatch = YearRegex.Match(meta); + if (yearMatch.Success) + int.TryParse(yearMatch.Value, out itemYear); + } + + if (string.IsNullOrWhiteSpace(itemTitle)) + continue; + + results.Add(new KlonSearchResult + { + Title = itemTitle, + Url = href, + Poster = NormalizeUrl(KlonHost, poster), + Year = itemYear + }); + } + } + + if (results.Count == 0) + { + var anchors = doc.DocumentNode.SelectNodes("//a[.//span[contains(@class,'searchheading')]]"); + if (anchors != null) + { + foreach (var anchor in anchors) + { + string href = NormalizeUrl(KlonHost, anchor.GetAttributeValue("href", null)); + if (string.IsNullOrWhiteSpace(href) || !seen.Add(href)) + continue; + + string itemTitle = CleanText(anchor.SelectSingleNode(".//span[contains(@class,'searchheading')]")?.InnerText); + if (string.IsNullOrWhiteSpace(itemTitle)) + continue; + + results.Add(new KlonSearchResult + { + Title = itemTitle, + Url = href, + Poster = string.Empty, + Year = 0 + }); + } + } + } + + return results; + } + catch (Exception ex) + { + _onLog($"Makhno KlonFUN: помилка запиту '{query}' - {ex.Message}"); + return null; + } + } + public async Task> SearchUaTUT(string query, string imdbId = null) { try @@ -196,6 +450,33 @@ namespace Makhno return src; } + private static string NormalizeUrl(string host, string url) + { + if (string.IsNullOrWhiteSpace(url)) + return string.Empty; + + string value = WebUtility.HtmlDecode(url.Trim()); + + if (value.StartsWith("//")) + return $"https:{value}"; + + if (value.StartsWith("/")) + return host.TrimEnd('/') + value; + + if (!value.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + return host.TrimEnd('/') + "/" + value.TrimStart('/'); + + return value; + } + + private static string CleanText(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return string.Empty; + + return HtmlEntity.DeEntitize(value).Trim(); + } + public async Task GetPlayerData(string playerUrl) { if (string.IsNullOrEmpty(playerUrl)) @@ -747,13 +1028,31 @@ namespace Makhno return itemYear.HasValue && itemYear.Value == year.Value; } + private bool YearMatch(KlonSearchResult item, int? year) + { + if (year == null || item == null || item.Year <= 0) + return false; + + return item.Year == 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); + return TitleMatch(item.Title, item.TitleEn, title, titleEn); + } + + private bool TitleMatch(string itemTitle, string title, string titleEn) + { + return TitleMatch(itemTitle, null, title, titleEn); + } + + private bool TitleMatch(string itemTitleRaw, string itemTitleEnRaw, string title, string titleEn) + { + string itemTitle = NormalizeTitle(itemTitleRaw); + string itemTitleEn = NormalizeTitle(itemTitleEnRaw); string targetTitle = NormalizeTitle(title); string targetTitleEn = NormalizeTitle(titleEn); @@ -788,7 +1087,7 @@ namespace Makhno return null; } - public async Task<(JObject item, string mediaType)?> FetchTmdbByImdb(string imdbId, int? year) + public async Task<(JObject item, string mediaType)?> FetchTmdbByImdb(string imdbId, int? year, bool isSerial) { if (string.IsNullOrWhiteSpace(imdbId)) return null; @@ -822,10 +1121,16 @@ namespace Makhno if (candidates.Count == 0) return null; + string preferredMediaType = isSerial ? "tv" : "movie"; + var orderedCandidates = candidates + .Where(c => c.mediaType == preferredMediaType) + .Concat(candidates.Where(c => c.mediaType != preferredMediaType)) + .ToList(); + if (year.HasValue) { string yearText = year.Value.ToString(); - foreach (var candidate in candidates) + foreach (var candidate in orderedCandidates) { string dateValue = candidate.mediaType == "movie" ? candidate.item.Value("release_date") @@ -836,7 +1141,7 @@ namespace Makhno } } - return candidates[0]; + return orderedCandidates[0]; } catch (Exception ex) { diff --git a/Makhno/ModInit.cs b/Makhno/ModInit.cs index e229339..3029b71 100644 --- a/Makhno/ModInit.cs +++ b/Makhno/ModInit.cs @@ -23,7 +23,7 @@ namespace Makhno { public class ModInit { - public static double Version => 2.0; + public static double Version => 2.1; public static OnlinesSettings Makhno; public static bool ApnHostProvided; diff --git a/Makhno/Models/MakhnoModels.cs b/Makhno/Models/MakhnoModels.cs index 9bbdb5a..60466a9 100644 --- a/Makhno/Models/MakhnoModels.cs +++ b/Makhno/Models/MakhnoModels.cs @@ -30,6 +30,14 @@ namespace Makhno.Models public string Category { get; set; } } + public class KlonSearchResult + { + public string Title { get; set; } + public string Url { get; set; } + public string Poster { get; set; } + public int Year { get; set; } + } + public class PlayerData { public string File { get; set; }