feat: Integrate KlonFUN service for content resolution and player URL extraction, update Wormhole host, and simplify EnrichWormhole parameters.

This commit is contained in:
Felix 2026-02-25 16:07:13 +02:00
parent b7aaf0cc93
commit 56eb95be3d
4 changed files with 371 additions and 27 deletions

View File

@ -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<List<KlonSearchResult>>(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<string>(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<List<SearchResult>>(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<long?>("id");
if (!tmdbId.HasValue)
return;
string original = item.Value<string>("original_title")
?? item.Value<string>("original_name")
?? resolved.Selected.TitleEn
?? originalTitle
?? title;
string resultTitle = resolved.Selected.Title
?? item.Value<string>("title")
?? item.Value<string>("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);

View File

@ -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<List<KlonSearchResult>> 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<KlonSearchResult>();
var seen = new HashSet<string>(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<KlonSearchResult>();
}
public KlonSearchResult SelectKlonFUNItem(List<KlonSearchResult> 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<string> GetKlonFUNPlayerUrl(string itemUrl)
{
if (string.IsNullOrWhiteSpace(itemUrl))
return null;
try
{
var headers = new List<HeadersModel>()
{
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<List<KlonSearchResult>> SearchKlonFUNByQuery(string query)
{
if (string.IsNullOrWhiteSpace(query))
return null;
try
{
var headers = new List<HeadersModel>()
{
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<KlonSearchResult>();
var seen = new HashSet<string>(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<List<SearchResult>> 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<PlayerData> 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<string>("release_date")
@ -836,7 +1141,7 @@ namespace Makhno
}
}
return candidates[0];
return orderedCandidates[0];
}
catch (Exception ex)
{

View File

@ -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;

View File

@ -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; }