mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-06-17 12:08:54 +00:00
Move PlayerPayload class into PlayerJsDecoder.cs and rename namespace to Shared.Engine. Remove linked source file references from anime projects (AnimeON, Mikai, NMoonAnime) to prevent duplicate compilation and ensure single source of truth through Shared.dll.
517 lines
18 KiB
C#
517 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using System.Threading.Tasks;
|
|
using System.Web;
|
|
using System.Net;
|
|
using System.Text.RegularExpressions;
|
|
using LME.Mikai.Models;
|
|
using Shared;
|
|
using Shared.Engine;
|
|
using Shared.Models;
|
|
using Shared.Models.Online.Settings;
|
|
using Shared.Models.Templates;
|
|
|
|
namespace LME.Mikai
|
|
{
|
|
public class AshdiStream
|
|
{
|
|
public string Title { get; set; }
|
|
public string Link { get; set; }
|
|
public SubtitleTpl Subtitles { get; set; }
|
|
}
|
|
|
|
public class MikaiInvoke
|
|
{
|
|
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<string> _onLog;
|
|
private readonly ProxyManager _proxyManager;
|
|
private readonly HttpHydra _httpHydra;
|
|
|
|
public MikaiInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
|
|
{
|
|
_init = init;
|
|
_hybridCache = hybridCache;
|
|
_onLog = onLog;
|
|
_proxyManager = proxyManager;
|
|
_httpHydra = httpHydra;
|
|
}
|
|
|
|
public async Task<List<MikaiAnime>> Search(string title, string original_title, int year)
|
|
{
|
|
string memKey = $"Mikai:search:{title}:{original_title}:{year}";
|
|
if (_hybridCache.TryGetValue(memKey, out List<MikaiAnime> cached))
|
|
return cached;
|
|
|
|
try
|
|
{
|
|
async Task<List<MikaiAnime>> FindAnime(string query)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(query))
|
|
return null;
|
|
|
|
string searchUrl = $"{_init.apihost}/anime/search?page=1&limit=24&sort=year&order=desc&name={HttpUtility.UrlEncode(query)}";
|
|
var headers = DefaultHeaders();
|
|
|
|
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {searchUrl}");
|
|
string json = await HttpGet(searchUrl, headers);
|
|
if (string.IsNullOrEmpty(json))
|
|
return null;
|
|
|
|
var response = JsonSerializer.Deserialize<SearchResponse>(json);
|
|
if (response?.Result == null || response.Result.Count == 0)
|
|
return null;
|
|
|
|
if (year > 0)
|
|
{
|
|
var byYear = response.Result.Where(r => r.Year == year).ToList();
|
|
if (byYear.Count > 0)
|
|
return byYear;
|
|
}
|
|
|
|
return response.Result;
|
|
}
|
|
|
|
var results = await FindAnime(title) ?? await FindAnime(original_title);
|
|
if (results == null || results.Count == 0)
|
|
return null;
|
|
|
|
_hybridCache.Set(memKey, results, cacheTime(10, init: _init));
|
|
return results;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_onLog($"Mikai Search error: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<MikaiAnime> GetDetails(int id)
|
|
{
|
|
string memKey = $"Mikai:details:{id}";
|
|
if (_hybridCache.TryGetValue(memKey, out MikaiAnime cached))
|
|
return cached;
|
|
|
|
try
|
|
{
|
|
string url = $"{_init.apihost}/anime/{id}";
|
|
var headers = DefaultHeaders();
|
|
|
|
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {url}");
|
|
string json = await HttpGet(url, headers);
|
|
if (string.IsNullOrEmpty(json))
|
|
return null;
|
|
|
|
var response = JsonSerializer.Deserialize<DetailResponse>(json);
|
|
if (response?.Result == null)
|
|
return null;
|
|
|
|
_hybridCache.Set(memKey, response.Result, cacheTime(20, init: _init));
|
|
return response.Result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_onLog($"Mikai Details error: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<string> ResolveVideoUrl(string url, bool disableAshdiMultivoiceForVod = false)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(url))
|
|
return null;
|
|
|
|
if (url.Contains("moonanime.art", StringComparison.OrdinalIgnoreCase))
|
|
return await ParseMoonAnimePage(url);
|
|
|
|
if (url.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase))
|
|
return await ParseAshdiPage(url, disableAshdiMultivoiceForVod);
|
|
|
|
return url;
|
|
}
|
|
|
|
public async Task<string> ParseMoonAnimePage(string url)
|
|
{
|
|
try
|
|
{
|
|
string requestUrl = url;
|
|
if (!requestUrl.Contains("player=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
requestUrl = requestUrl.Contains("?")
|
|
? $"{requestUrl}&player=mikai.me"
|
|
: $"{requestUrl}?player=mikai.me";
|
|
}
|
|
|
|
var headers = new List<HeadersModel>()
|
|
{
|
|
new HeadersModel("User-Agent", "Mozilla/5.0"),
|
|
new HeadersModel("Referer", _init.host)
|
|
};
|
|
|
|
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
|
|
string html = await HttpGet(requestUrl, headers);
|
|
if (string.IsNullOrEmpty(html))
|
|
return null;
|
|
|
|
var payload = PlayerJsDecoder.ExtractPlayerPayload(html);
|
|
if (payload?.FilePayload == null)
|
|
return null;
|
|
|
|
var streamUrls = ExtractStreamUrls(payload.FilePayload);
|
|
return streamUrls?.FirstOrDefault();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_onLog($"Mikai ParseMoonAnimePage error: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private List<string> ExtractStreamUrls(object filePayload)
|
|
{
|
|
var urls = new List<string>();
|
|
if (filePayload == null)
|
|
return urls;
|
|
|
|
// Обробка string значення
|
|
if (filePayload is string strPayload)
|
|
{
|
|
urls.Add(strPayload);
|
|
return urls;
|
|
}
|
|
|
|
// Обробка JsonValue
|
|
if (filePayload is JsonValue jsonValue && jsonValue.TryGetValue<string>(out string strValue))
|
|
{
|
|
urls.Add(strValue);
|
|
return urls;
|
|
}
|
|
|
|
// Обробка JsonObject — витягти 'file' поле
|
|
if (filePayload is JsonObject objPayload)
|
|
{
|
|
if (objPayload.TryGetPropertyValue("file", out JsonNode fileNode))
|
|
{
|
|
string fileStr = fileNode?.ToString();
|
|
if (!string.IsNullOrEmpty(fileStr))
|
|
urls.Add(fileStr);
|
|
}
|
|
return urls;
|
|
}
|
|
|
|
// Обробка JsonArray
|
|
if (filePayload is JsonArray arrayPayload)
|
|
{
|
|
foreach (var item in arrayPayload)
|
|
{
|
|
if (item is JsonObject itemObj && itemObj.TryGetPropertyValue("file", out JsonNode fileProp))
|
|
{
|
|
string fileStr = fileProp?.ToString();
|
|
if (!string.IsNullOrEmpty(fileStr))
|
|
urls.Add(fileStr);
|
|
}
|
|
else if (item is JsonValue itemValue && itemValue.TryGetValue<string>(out string itemStr))
|
|
{
|
|
if (!string.IsNullOrEmpty(itemStr))
|
|
urls.Add(itemStr);
|
|
}
|
|
}
|
|
return urls;
|
|
}
|
|
|
|
return urls;
|
|
}
|
|
|
|
string AshdiRequestUrl(string url)
|
|
{
|
|
if (!ApnHelper.IsAshdiUrl(url))
|
|
return url;
|
|
|
|
if (!string.IsNullOrWhiteSpace(_init.webcorshost))
|
|
return url;
|
|
|
|
return ApnHelper.WrapUrl(_init, url);
|
|
}
|
|
|
|
public async Task<string> ParseAshdiPage(string url, bool disableAshdiMultivoiceForVod = false)
|
|
{
|
|
var streams = await ParseAshdiPageStreams(url, disableAshdiMultivoiceForVod);
|
|
return streams?.FirstOrDefault()?.Link;
|
|
}
|
|
|
|
public async Task<List<AshdiStream>> ParseAshdiPageStreams(string url, bool disableAshdiMultivoiceForVod = false)
|
|
{
|
|
var streams = new List<AshdiStream>();
|
|
try
|
|
{
|
|
var headers = new List<HeadersModel>()
|
|
{
|
|
new HeadersModel("User-Agent", "Mozilla/5.0"),
|
|
new HeadersModel("Referer", "https://ashdi.vip/")
|
|
};
|
|
|
|
string requestUrl = AshdiRequestUrl(WithAshdiMultivoice(url, enable: !disableAshdiMultivoiceForVod));
|
|
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
|
|
string html = await HttpGet(requestUrl, headers);
|
|
if (string.IsNullOrEmpty(html))
|
|
return streams;
|
|
|
|
string rawArray = ExtractPlayerFileArray(html);
|
|
if (!string.IsNullOrWhiteSpace(rawArray))
|
|
{
|
|
string json = WebUtility.HtmlDecode(rawArray)
|
|
.Replace("\\/", "/")
|
|
.Replace("\\'", "'")
|
|
.Replace("\\\"", "\"");
|
|
|
|
using var jsonDoc = JsonDocument.Parse(json);
|
|
if (jsonDoc.RootElement.ValueKind == JsonValueKind.Array)
|
|
{
|
|
int index = 1;
|
|
foreach (var item in jsonDoc.RootElement.EnumerateArray())
|
|
{
|
|
if (!item.TryGetProperty("file", out var fileProp))
|
|
continue;
|
|
|
|
string file = fileProp.GetString();
|
|
if (string.IsNullOrWhiteSpace(file))
|
|
continue;
|
|
|
|
string rawTitle = item.TryGetProperty("title", out var titleProp) ? titleProp.GetString() : null;
|
|
streams.Add(new AshdiStream
|
|
{
|
|
Title = BuildDisplayTitle(rawTitle, file, index),
|
|
Link = file,
|
|
Subtitles = ApnHelper.ParseSubtitles(item.TryGetProperty("subtitle", out var subtitleProp) ? subtitleProp.GetString() : null)
|
|
});
|
|
index++;
|
|
}
|
|
|
|
if (streams.Count > 0)
|
|
return streams;
|
|
}
|
|
}
|
|
|
|
var match = Regex.Match(html, @"file\s*:\s*['""]([^'""]+)['""]");
|
|
if (match.Success)
|
|
{
|
|
string file = match.Groups[1].Value;
|
|
streams.Add(new AshdiStream
|
|
{
|
|
Title = BuildDisplayTitle("Основне джерело", file, 1),
|
|
Link = file,
|
|
Subtitles = ApnHelper.ParseSubtitles(ApnHelper.ExtractPlayerSubtitle(html))
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_onLog($"Mikai ParseAshdiPage error: {ex.Message}");
|
|
}
|
|
|
|
return streams;
|
|
}
|
|
|
|
private List<HeadersModel> DefaultHeaders()
|
|
{
|
|
return new List<HeadersModel>()
|
|
{
|
|
new HeadersModel("User-Agent", "Mozilla/5.0"),
|
|
new HeadersModel("Referer", _init.host),
|
|
new HeadersModel("Accept", "application/json")
|
|
};
|
|
}
|
|
|
|
private static string WithAshdiMultivoice(string url, bool enable = true)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(url))
|
|
return url;
|
|
|
|
if (!enable)
|
|
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 BuildDisplayTitle(string rawTitle, string link, int index)
|
|
{
|
|
string normalized = string.IsNullOrWhiteSpace(rawTitle) ? $"Варіант {index}" : StripMoviePrefix(WebUtility.HtmlDecode(rawTitle).Trim());
|
|
string qualityTag = DetectQualityTag($"{normalized} {link}");
|
|
if (string.IsNullOrWhiteSpace(qualityTag))
|
|
return normalized;
|
|
|
|
if (normalized.StartsWith("[4K]", StringComparison.OrdinalIgnoreCase) || normalized.StartsWith("[FHD]", StringComparison.OrdinalIgnoreCase))
|
|
return normalized;
|
|
|
|
return $"{qualityTag} {normalized}";
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private static string ExtractPlayerFileArray(string html)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(html))
|
|
return null;
|
|
|
|
int searchIndex = 0;
|
|
while (searchIndex >= 0 && searchIndex < html.Length)
|
|
{
|
|
int fileIndex = html.IndexOf("file", searchIndex, StringComparison.OrdinalIgnoreCase);
|
|
if (fileIndex < 0)
|
|
return null;
|
|
|
|
int colonIndex = html.IndexOf(':', fileIndex);
|
|
if (colonIndex < 0)
|
|
return null;
|
|
|
|
int startIndex = colonIndex + 1;
|
|
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
|
|
startIndex++;
|
|
|
|
if (startIndex < html.Length && (html[startIndex] == '\'' || html[startIndex] == '"'))
|
|
{
|
|
startIndex++;
|
|
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
|
|
startIndex++;
|
|
}
|
|
|
|
if (startIndex >= html.Length || html[startIndex] != '[')
|
|
{
|
|
searchIndex = fileIndex + 4;
|
|
continue;
|
|
}
|
|
|
|
return ExtractBracketArray(html, startIndex);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string ExtractBracketArray(string text, int startIndex)
|
|
{
|
|
if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[')
|
|
return null;
|
|
|
|
int depth = 0;
|
|
bool inString = false;
|
|
bool escaped = false;
|
|
char quoteChar = '\0';
|
|
|
|
for (int i = startIndex; i < text.Length; i++)
|
|
{
|
|
char ch = text[i];
|
|
|
|
if (inString)
|
|
{
|
|
if (escaped)
|
|
{
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
|
|
if (ch == '\\')
|
|
{
|
|
escaped = 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 Task<string> HttpGet(string url, List<HeadersModel> headers)
|
|
{
|
|
if (_httpHydra != null)
|
|
return _httpHydra.Get(url, newheaders: headers);
|
|
|
|
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
|
}
|
|
|
|
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
|
|
{
|
|
if (init != null && init.rhub && rhub != -1)
|
|
return TimeSpan.FromMinutes(rhub);
|
|
|
|
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
|
|
if (ctime > multiaccess)
|
|
ctime = multiaccess;
|
|
|
|
return TimeSpan.FromMinutes(ctime);
|
|
}
|
|
}
|
|
}
|