refactor: Remove KlonFUN integration and Wormhole enrichment, simplifying play source resolution to only use Wormhole.

This commit is contained in:
Felix 2026-02-27 11:43:07 +02:00
parent 6cdddcbf30
commit ae39beb8b6
3 changed files with 14 additions and 578 deletions

View File

@ -45,25 +45,10 @@ namespace Makhno
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager);
var resolved = await ResolvePlaySource(imdb_id, title, original_title, year, serial, invoke);
var resolved = await ResolvePlaySource(imdb_id, serial, invoke);
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
return OnError();
if (resolved.ShouldEnrich)
{
_ = Task.Run(async () =>
{
try
{
await EnrichWormhole(imdb_id, year, resolved, invoke);
}
catch (Exception ex)
{
OnLog($"Makhno wormhole enrich failed: {ex.Message}");
}
});
}
if (resolved.IsSerial)
return await HandleSerial(resolved.PlayUrl, imdb_id, title, original_title, year, t, season, rjson, invoke);
@ -84,7 +69,7 @@ namespace Makhno
OnLog($"Makhno Play: {title} (s={s}, season={season}, t={t}, episodeId={episodeId}) play={play}");
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager);
var resolved = await ResolvePlaySource(imdb_id, title, original_title, year, serial: 1, invoke);
var resolved = await ResolvePlaySource(imdb_id, serial: 1, invoke);
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
return OnError();
@ -142,7 +127,7 @@ namespace Makhno
OnLog($"Makhno PlayMovie: {title} ({year}) play={play}");
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager);
var resolved = await ResolvePlaySource(imdb_id, title, original_title, year, serial: 0, invoke);
var resolved = await ResolvePlaySource(imdb_id, serial: 0, invoke);
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
return OnError();
@ -455,68 +440,24 @@ namespace Makhno
return result;
}
private async Task<ResolveResult> ResolvePlaySource(string imdbId, string title, string originalTitle, int year, int serial, MakhnoInvoke invoke)
private async Task<ResolveResult> ResolvePlaySource(string imdbId, int serial, MakhnoInvoke invoke)
{
string playUrl = null;
if (string.IsNullOrWhiteSpace(imdbId))
return null;
if (!string.IsNullOrEmpty(imdbId))
string cacheKey = $"makhno:wormhole:{imdbId}";
string playUrl = await InvokeCache<string>(cacheKey, TimeSpan.FromMinutes(5), async () =>
{
string cacheKey = $"makhno:wormhole:{imdbId}";
playUrl = await InvokeCache<string>(cacheKey, TimeSpan.FromMinutes(5), async () =>
{
return await invoke.GetWormholePlay(imdbId);
});
if (!string.IsNullOrEmpty(playUrl))
{
return new ResolveResult
{
PlayUrl = playUrl,
IsSerial = IsSerialByUrl(playUrl, serial),
ShouldEnrich = false
};
}
}
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);
return await invoke.GetWormholePlay(imdbId);
});
if (klonSearchResults != null && klonSearchResults.Count > 0)
if (!string.IsNullOrEmpty(playUrl))
{
var klonSelected = invoke.SelectKlonFUNItem(klonSearchResults, year > 0 ? year : null, title, originalTitle);
if (klonSelected != null && !string.IsNullOrWhiteSpace(klonSelected.Url))
return new ResolveResult
{
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)
};
}
}
PlayUrl = playUrl,
IsSerial = IsSerialByUrl(playUrl, serial)
};
}
return null;
@ -533,38 +474,6 @@ namespace Makhno
return url.Contains("/serial/", StringComparison.OrdinalIgnoreCase);
}
private async Task EnrichWormhole(string imdbId, int year, ResolveResult resolved, MakhnoInvoke invoke)
{
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))
yearValue = parsedYear;
var tmdbResult = await invoke.FetchTmdbByImdb(imdbId, yearValue, resolved.IsSerial);
if (tmdbResult == null)
return;
var (item, _) = tmdbResult.Value;
var tmdbId = item.Value<long?>("id");
if (!tmdbId.HasValue)
return;
string mediaType = resolved.IsSerial ? "tv" : "movie";
int serialValue = resolved.IsSerial ? 1 : 0;
var payload = new
{
imdb_id = imdbId,
_id = $"{mediaType}:{tmdbId.Value}",
serial = serialValue,
ashdi = resolved.AshdiPath
};
await invoke.PostWormholeAsync(payload);
}
private static string StripLampacArgs(string url)
{
if (string.IsNullOrEmpty(url))
@ -606,10 +515,7 @@ namespace Makhno
private class ResolveResult
{
public string PlayUrl { get; set; }
public string AshdiPath { get; set; }
public SearchResult Selected { get; set; }
public bool IsSerial { get; set; }
public bool ShouldEnrich { get; set; }
}
}
}

View File

@ -2,10 +2,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
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;
@ -19,11 +17,8 @@ namespace Makhno
public class MakhnoInvoke
{
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;
@ -65,284 +60,6 @@ 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;
}
}
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))
@ -809,157 +526,6 @@ namespace Makhno
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('/')}";
}
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(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);
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();
}
public async Task<(JObject item, string mediaType)?> FetchTmdbByImdb(string imdbId, int? year, bool isSerial)
{
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<HeadersModel>()
{
new HeadersModel("User-Agent", Http.UserAgent)
};
JObject payload = await Http.Get<JObject>(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<JObject>())
candidates.Add((item, "movie"));
foreach (var item in tvResults.OfType<JObject>())
candidates.Add((item, "tv"));
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 orderedCandidates)
{
string dateValue = candidate.mediaType == "movie"
? candidate.item.Value<string>("release_date")
: candidate.item.Value<string>("first_air_date");
if (!string.IsNullOrWhiteSpace(dateValue) && dateValue.StartsWith(yearText, StringComparison.Ordinal))
return candidate;
}
}
return orderedCandidates[0];
}
catch (Exception ex)
{
_onLog($"Makhno TMDB fetch failed: {ex.Message}");
return null;
}
}
public async Task<bool> PostWormholeAsync(object payload)
{
try
{
var headers = new List<HeadersModel>()
{
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; }

View File

@ -1,43 +1,7 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Makhno.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 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; }