mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-04-16 17:32:20 +00:00
Replace regex-based file array extraction with a more robust bracket matching algorithm that properly handles nested structures. The new implementation uses manual parsing to track bracket depth, ensuring correct extraction of JSON arrays from HTML content.
639 lines
22 KiB
C#
639 lines
22 KiB
C#
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 readonly OnlinesSettings _init;
|
|
private readonly IHybridCache _hybridCache;
|
|
private readonly Action<string> _onLog;
|
|
private readonly ProxyManager _proxyManager;
|
|
|
|
public MakhnoInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
|
{
|
|
_init = init;
|
|
_hybridCache = hybridCache;
|
|
_onLog = onLog;
|
|
_proxyManager = proxyManager;
|
|
}
|
|
|
|
public async Task<string> GetWormholePlay(string imdbId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(imdbId))
|
|
return null;
|
|
|
|
string url = $"{WormholeHost}?imdb_id={imdbId}";
|
|
try
|
|
{
|
|
var headers = new List<HeadersModel>()
|
|
{
|
|
new HeadersModel("User-Agent", Http.UserAgent)
|
|
};
|
|
|
|
string response = await Http.Get(url, timeoutSeconds: 4, headers: headers, proxy: _proxyManager.Get());
|
|
if (string.IsNullOrWhiteSpace(response))
|
|
return null;
|
|
|
|
var payload = JsonConvert.DeserializeObject<WormholeResponse>(response);
|
|
return string.IsNullOrWhiteSpace(payload?.play) ? null : payload.play;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_onLog($"Makhno wormhole error: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<List<SearchResult>> 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<SearchResult>();
|
|
}
|
|
|
|
return new List<SearchResult>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_onLog($"Makhno UaTUT search error: {ex.Message}");
|
|
return new List<SearchResult>();
|
|
}
|
|
}
|
|
|
|
private async Task<List<SearchResult>> PerformSearch(string searchUrl, string query)
|
|
{
|
|
string url = $"{searchUrl}?q={WebUtility.UrlEncode(query)}";
|
|
_onLog($"Makhno UaTUT searching: {url}");
|
|
|
|
var headers = new List<HeadersModel>()
|
|
{
|
|
new HeadersModel("User-Agent", Http.UserAgent)
|
|
};
|
|
|
|
var response = await Http.Get(url, headers: headers, proxy: _proxyManager.Get());
|
|
|
|
if (string.IsNullOrEmpty(response))
|
|
return null;
|
|
|
|
try
|
|
{
|
|
var results = JsonConvert.DeserializeObject<List<SearchResult>>(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<string> GetMoviePageContent(string movieId)
|
|
{
|
|
try
|
|
{
|
|
string url = $"{_init.apihost}/{movieId}";
|
|
_onLog($"Makhno UaTUT getting movie page: {url}");
|
|
|
|
var headers = new List<HeadersModel>()
|
|
{
|
|
new HeadersModel("User-Agent", Http.UserAgent)
|
|
};
|
|
var response = await Http.Get(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, @"<iframe[^>]*id=[""']vip-player[""'][^>]*src=[""']([^""']+)", RegexOptions.IgnoreCase);
|
|
if (match.Success)
|
|
return NormalizePlayerUrl(match.Groups[1].Value);
|
|
|
|
match = Regex.Match(moviePageContent, @"<iframe[^>]*id=[""']alt-player[""'][^>]*src=[""']([^""']+)", RegexOptions.IgnoreCase);
|
|
if (match.Success)
|
|
return NormalizePlayerUrl(match.Groups[1].Value);
|
|
|
|
var iframeMatches = Regex.Matches(moviePageContent, @"<iframe[^>]*(?: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<PlayerData> GetPlayerData(string playerUrl)
|
|
{
|
|
if (string.IsNullOrEmpty(playerUrl))
|
|
return null;
|
|
|
|
try
|
|
{
|
|
string requestUrl = playerUrl;
|
|
var headers = new List<HeadersModel>()
|
|
{
|
|
new HeadersModel("User-Agent", Http.UserAgent)
|
|
};
|
|
|
|
if (playerUrl.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
headers.Add(new HeadersModel("Referer", "https://ashdi.vip/"));
|
|
}
|
|
|
|
if (ApnHelper.IsAshdiUrl(playerUrl) && ApnHelper.IsEnabled(_init))
|
|
requestUrl = ApnHelper.WrapUrl(_init, playerUrl);
|
|
|
|
_onLog($"Makhno getting player data from: {requestUrl}");
|
|
|
|
var response = await Http.Get(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("["))
|
|
{
|
|
var posterMatch = Regex.Match(html, @"poster:[""']([^""']+)[""']", RegexOptions.IgnoreCase);
|
|
return new PlayerData
|
|
{
|
|
File = fileMatch.Groups[1].Value,
|
|
Poster = posterMatch.Success ? posterMatch.Groups[1].Value : null,
|
|
Voices = new List<Voice>()
|
|
};
|
|
}
|
|
|
|
var m3u8Match = Regex.Match(html, @"(https?://[^""'\s>]+\.m3u8[^""'\s>]*)", RegexOptions.IgnoreCase);
|
|
if (m3u8Match.Success)
|
|
{
|
|
return new PlayerData
|
|
{
|
|
File = m3u8Match.Groups[1].Value,
|
|
Poster = null,
|
|
Voices = new List<Voice>()
|
|
};
|
|
}
|
|
|
|
var sourceMatch = Regex.Match(html, @"<source[^>]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase);
|
|
if (sourceMatch.Success)
|
|
{
|
|
return new PlayerData
|
|
{
|
|
File = sourceMatch.Groups[1].Value,
|
|
Poster = null,
|
|
Voices = new List<Voice>()
|
|
};
|
|
}
|
|
|
|
string jsonData = ExtractPlayerJson(html);
|
|
if (!string.IsNullOrEmpty(jsonData))
|
|
{
|
|
return new PlayerData
|
|
{
|
|
File = null,
|
|
Poster = null,
|
|
Voices = ParseVoicesJson(jsonData)
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_onLog($"Makhno ParsePlayerData error: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private List<Voice> ParseVoicesJson(string jsonData)
|
|
{
|
|
try
|
|
{
|
|
var voicesArray = JsonConvert.DeserializeObject<List<JObject>>(jsonData);
|
|
var voices = new List<Voice>();
|
|
|
|
if (voicesArray == null)
|
|
return voices;
|
|
|
|
foreach (var voiceGroup in voicesArray)
|
|
{
|
|
var voice = new Voice
|
|
{
|
|
Name = voiceGroup["title"]?.ToString(),
|
|
Seasons = new List<Season>()
|
|
};
|
|
|
|
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<Episode>();
|
|
|
|
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<Voice>();
|
|
}
|
|
}
|
|
|
|
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 fileIndex = html.IndexOf("file", StringComparison.OrdinalIgnoreCase);
|
|
if (fileIndex < 0)
|
|
return -1;
|
|
|
|
int bracketIndex = html.IndexOf('[', fileIndex);
|
|
return bracketIndex;
|
|
}
|
|
|
|
private string ExtractBracketArray(string text, int startIndex)
|
|
{
|
|
if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[')
|
|
return null;
|
|
|
|
int depth = 0;
|
|
for (int i = startIndex; i < text.Length; i++)
|
|
{
|
|
char ch = text[i];
|
|
if (ch == '[')
|
|
depth++;
|
|
else 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;
|
|
}
|
|
|
|
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<string> 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<SearchResult> 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<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;
|
|
|
|
if (year.HasValue)
|
|
{
|
|
string yearText = year.Value.ToString();
|
|
foreach (var candidate in candidates)
|
|
{
|
|
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 candidates[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(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; }
|
|
}
|
|
}
|
|
}
|