feat(moon_decoder): add moon anime decryption and multi-quality streams

This commit is contained in:
Felix 2026-05-30 10:09:56 +03:00
parent 6a398317a4
commit 41ba6dc878
5 changed files with 383 additions and 30 deletions

View File

@ -169,15 +169,82 @@ namespace LME.AnimeON
return JsonSerializer.Deserialize<EpisodeModel>(episodesJson);
}
#region MoonAnime Decryption
private static string CleanMoonUrl(string url)
{
if (string.IsNullOrEmpty(url))
return url;
string cleaned = Regex.Replace(url, @"([?&])player=[^&]*", "$1", RegexOptions.IgnoreCase);
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned;
}
public static string MoonDecode(string base64Input)
{
try
{
byte[] raw = Convert.FromBase64String(base64Input);
const int KeySize = 32;
const int HeaderSize = 1 + KeySize;
if (raw.Length < HeaderSize)
return null;
byte state = raw[0];
byte[] key = new byte[KeySize];
Array.Copy(raw, 1, key, 0, KeySize);
int payloadLen = raw.Length - HeaderSize;
byte[] payload = new byte[payloadLen];
Array.Copy(raw, HeaderSize, payload, 0, payloadLen);
for (int i = 0; i < payload.Length; i++)
{
byte encrypted = payload[i];
byte keyByte = key[i % KeySize];
payload[i] = (byte)(encrypted ^ keyByte ^ state);
state = (byte)((encrypted + keyByte) & 0xFF);
}
return Encoding.UTF8.GetString(payload);
}
catch
{
return null;
}
}
public static string MoonXorDecrypt(string file, string key)
{
try
{
byte[] keyBytes = Encoding.UTF8.GetBytes(key);
byte[] data = Convert.FromBase64String(file);
for (int i = 0; i < data.Length; i++)
{
data[i] = (byte)(data[i] ^ keyBytes[i % keyBytes.Length]);
}
return Encoding.UTF8.GetString(data);
}
catch
{
return null;
}
}
#endregion
public async Task<string> ParseMoonAnimePage(string url)
{
try
{
string requestUrl = $"{url}?player=animeon.club";
string requestUrl = CleanMoonUrl(url);
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://animeon.club/")
new HeadersModel("User-Agent", "Mozilla/5.0")
};
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
@ -185,12 +252,56 @@ namespace LME.AnimeON
if (string.IsNullOrEmpty(html))
return null;
var payload = PlayerJsDecoder.ExtractPlayerPayload(html);
if (payload?.FilePayload == null)
return null;
var atobMatch = Regex.Match(html, @"=atob\(""([^""]+)""\)");
if (!atobMatch.Success)
{
atobMatch = Regex.Match(html, @"=atob\('([^']+)'\)");
}
var streamUrls = ExtractStreamUrls(payload.FilePayload);
return streamUrls?.FirstOrDefault();
if (atobMatch.Success)
{
string blob = atobMatch.Groups[1].Value;
string decryptedJs = MoonDecode(blob);
if (!string.IsNullOrEmpty(decryptedJs))
{
var keyMatch = Regex.Match(decryptedJs, @"var k=""([^""]+)""");
if (!keyMatch.Success)
keyMatch = Regex.Match(decryptedJs, @"var k='([^']+)'");
var fileMatch = Regex.Match(decryptedJs, @"file\s*:\s*_0xd\(""([^""]+)""\)");
if (!fileMatch.Success)
fileMatch = Regex.Match(decryptedJs, @"file\s*:\s*_0xd\('([^']+)'\)");
if (keyMatch.Success && fileMatch.Success)
{
string key = keyMatch.Groups[1].Value;
string fileEncrypted = fileMatch.Groups[1].Value;
string streams = MoonXorDecrypt(fileEncrypted, key);
if (!string.IsNullOrEmpty(streams))
{
return streams;
}
}
else
{
var directFileMatch = Regex.Match(decryptedJs, @"file\s*:\s*""([^""]+)""");
if (!directFileMatch.Success)
directFileMatch = Regex.Match(decryptedJs, @"file\s*:\s*'([^']+)'");
if (directFileMatch.Success)
{
return directFileMatch.Groups[1].Value;
}
}
}
}
var payload = PlayerJsDecoder.ExtractPlayerPayload(html);
if (payload?.FilePayload != null)
{
var streamUrls = ExtractStreamUrls(payload.FilePayload);
return streamUrls?.FirstOrDefault();
}
}
catch (Exception ex)
{

View File

@ -1,4 +1,4 @@
using System.Text.Json;
using System.Text.Json;
using Shared.Engine;
using System;
using System.Threading.Tasks;
@ -439,8 +439,59 @@ namespace LME.AnimeON.Controllers
catch { }
}
string streamUrl = BuildStreamUrl(init, streamLink, streamHeaders, forceProxy);
var streamQuality = new StreamQualityTpl();
string streamUrl = null;
if (!string.IsNullOrEmpty(streamLink) && (streamLink.StartsWith("[") || streamLink.Contains("]http")))
{
var bracketMatches = Regex.Matches(streamLink, @"\[(?<quality>[^\]]+)\](?<url>https?://[^,\[]+)", RegexOptions.IgnoreCase);
foreach (Match match in bracketMatches)
{
string quality = match.Groups["quality"].Value;
string urlVal = match.Groups["url"].Value?.Trim()?.TrimEnd(',');
if (string.IsNullOrWhiteSpace(urlVal))
continue;
var headers = streamHeaders;
if (urlVal.Contains("moonanime.art") || urlVal.Contains("mooncdn.space"))
{
headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://moonanime.art/")
};
}
string qualityStreamUrl = BuildStreamUrl(init, urlVal, headers, forceProxy);
streamQuality.Append(qualityStreamUrl, quality);
}
if (streamQuality.Any())
{
var first = streamQuality.Firts();
streamUrl = first.link;
}
}
if (string.IsNullOrEmpty(streamUrl))
{
var headers = streamHeaders;
if (!string.IsNullOrEmpty(streamLink) && (streamLink.Contains("moonanime.art") || streamLink.Contains("mooncdn.space")))
{
headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://moonanime.art/")
};
}
streamUrl = BuildStreamUrl(init, streamLink, headers, forceProxy);
}
OnLog("AnimeON Play: return call JSON");
if (streamQuality.Any())
{
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? string.Empty, subtitles: subtitleTpl, streamquality: streamQuality), "application/json; charset=utf-8"));
}
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? string.Empty, subtitles: subtitleTpl), "application/json; charset=utf-8"));
}

View File

@ -254,7 +254,58 @@ namespace LME.Mikai.Controllers
catch { }
}
string streamUrl = BuildStreamUrl(init, streamLink, streamHeaders, forceProxy);
var streamQuality = new StreamQualityTpl();
string streamUrl = null;
if (!string.IsNullOrEmpty(streamLink) && (streamLink.StartsWith("[") || streamLink.Contains("]http")))
{
var bracketMatches = Regex.Matches(streamLink, @"\[(?<quality>[^\]]+)\](?<url>https?://[^,\[]+)", RegexOptions.IgnoreCase);
foreach (Match match in bracketMatches)
{
string quality = match.Groups["quality"].Value;
string urlVal = match.Groups["url"].Value?.Trim()?.TrimEnd(',');
if (string.IsNullOrWhiteSpace(urlVal))
continue;
var headers = streamHeaders;
if (urlVal.Contains("moonanime.art") || urlVal.Contains("mooncdn.space"))
{
headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://moonanime.art/")
};
}
string qualityStreamUrl = BuildStreamUrl(init, urlVal, headers, forceProxy);
streamQuality.Append(qualityStreamUrl, quality);
}
if (streamQuality.Any())
{
var first = streamQuality.Firts();
streamUrl = first.link;
}
}
if (string.IsNullOrEmpty(streamUrl))
{
var headers = streamHeaders;
if (!string.IsNullOrEmpty(streamLink) && (streamLink.Contains("moonanime.art") || streamLink.Contains("mooncdn.space")))
{
headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://moonanime.art/")
};
}
streamUrl = BuildStreamUrl(init, streamLink, headers, forceProxy);
}
if (streamQuality.Any())
{
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? string.Empty, subtitles: subtitleTpl, streamquality: streamQuality), "application/json; charset=utf-8"));
}
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? string.Empty, subtitles: subtitleTpl), "application/json; charset=utf-8"));
}

View File

@ -137,22 +137,82 @@ namespace LME.Mikai
return url;
}
#region MoonAnime Decryption
private static string CleanMoonUrl(string url)
{
if (string.IsNullOrEmpty(url))
return url;
string cleaned = Regex.Replace(url, @"([?&])player=[^&]*", "$1", RegexOptions.IgnoreCase);
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned;
}
public static string MoonDecode(string base64Input)
{
try
{
byte[] raw = Convert.FromBase64String(base64Input);
const int KeySize = 32;
const int HeaderSize = 1 + KeySize;
if (raw.Length < HeaderSize)
return null;
byte state = raw[0];
byte[] key = new byte[KeySize];
Array.Copy(raw, 1, key, 0, KeySize);
int payloadLen = raw.Length - HeaderSize;
byte[] payload = new byte[payloadLen];
Array.Copy(raw, HeaderSize, payload, 0, payloadLen);
for (int i = 0; i < payload.Length; i++)
{
byte encrypted = payload[i];
byte keyByte = key[i % KeySize];
payload[i] = (byte)(encrypted ^ keyByte ^ state);
state = (byte)((encrypted + keyByte) & 0xFF);
}
return Encoding.UTF8.GetString(payload);
}
catch
{
return null;
}
}
public static string MoonXorDecrypt(string file, string key)
{
try
{
byte[] keyBytes = Encoding.UTF8.GetBytes(key);
byte[] data = Convert.FromBase64String(file);
for (int i = 0; i < data.Length; i++)
{
data[i] = (byte)(data[i] ^ keyBytes[i % keyBytes.Length]);
}
return Encoding.UTF8.GetString(data);
}
catch
{
return null;
}
}
#endregion
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";
}
string requestUrl = CleanMoonUrl(url);
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", _init.host)
new HeadersModel("User-Agent", "Mozilla/5.0")
};
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
@ -160,18 +220,63 @@ namespace LME.Mikai
if (string.IsNullOrEmpty(html))
return null;
var payload = PlayerJsDecoder.ExtractPlayerPayload(html);
if (payload?.FilePayload == null)
return null;
var atobMatch = Regex.Match(html, @"=atob\(""([^""]+)""\)");
if (!atobMatch.Success)
{
atobMatch = Regex.Match(html, @"=atob\('([^']+)'\)");
}
var streamUrls = ExtractStreamUrls(payload.FilePayload);
return streamUrls?.FirstOrDefault();
if (atobMatch.Success)
{
string blob = atobMatch.Groups[1].Value;
string decryptedJs = MoonDecode(blob);
if (!string.IsNullOrEmpty(decryptedJs))
{
var keyMatch = Regex.Match(decryptedJs, @"var k=""([^""]+)""");
if (!keyMatch.Success)
keyMatch = Regex.Match(decryptedJs, @"var k='([^']+)'");
var fileMatch = Regex.Match(decryptedJs, @"file\s*:\s*_0xd\(""([^""]+)""\)");
if (!fileMatch.Success)
fileMatch = Regex.Match(decryptedJs, @"file\s*:\s*_0xd\('([^']+)'\)");
if (keyMatch.Success && fileMatch.Success)
{
string key = keyMatch.Groups[1].Value;
string fileEncrypted = fileMatch.Groups[1].Value;
string streams = MoonXorDecrypt(fileEncrypted, key);
if (!string.IsNullOrEmpty(streams))
{
return streams;
}
}
else
{
var directFileMatch = Regex.Match(decryptedJs, @"file\s*:\s*""([^""]+)""");
if (!directFileMatch.Success)
directFileMatch = Regex.Match(decryptedJs, @"file\s*:\s*'([^']+)'");
if (directFileMatch.Success)
{
return directFileMatch.Groups[1].Value;
}
}
}
}
var payload = PlayerJsDecoder.ExtractPlayerPayload(html);
if (payload?.FilePayload != null)
{
var streamUrls = ExtractStreamUrls(payload.FilePayload);
return streamUrls?.FirstOrDefault();
}
}
catch (Exception ex)
{
_onLog($"Mikai ParseMoonAnimePage error: {ex.Message}");
return null;
}
return null;
}
private List<string> ExtractStreamUrls(object filePayload)

View File

@ -11,7 +11,7 @@ namespace LME.Common.Playerjs
{
public static class PlayerJsDecoder
{
private static readonly Regex _reAtobLiteral = new Regex(@"atob\(\s*(['""])(?<payload>[A-Za-z0-9+/=]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex _reAtobLiteral = new Regex(@"atob\(\s*(['""])(?<payload>[^'""]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex _reJsonParseHelper = new Regex(@"JSON\.parse\(\s*(?<fn>[A-Za-z_$][\w$]*)\(\s*(?<quote>['""])(?<payload>.*?)(\k<quote>)\s*\)\s*\)", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
private static readonly Regex _reHelperCall = new Regex(@"^\s*(?<fn>[A-Za-z_$][\w$]*)\(\s*(?<quote>['""])(?<payload>.*?)(\k<quote>)\s*\)\s*$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
private static readonly Regex _reTrailingComma = new Regex(@",\s*([}\]])", RegexOptions.Compiled);
@ -150,7 +150,10 @@ namespace LME.Common.Playerjs
if (!match.Success)
return null;
byte[] rawData = SafeBase64Decode(match.Groups["payload"].Value);
string rawBase64 = match.Groups["payload"].Value;
rawBase64 = Regex.Replace(rawBase64, @"\s+", "");
byte[] rawData = SafeBase64Decode(rawBase64);
if (rawData == null || rawData.Length <= 32)
return null;
@ -161,7 +164,39 @@ namespace LME.Common.Playerjs
for (int index = 0; index < encryptedData.Length; index++)
decoded[index] = (byte)(encryptedData[index] ^ key[index % key.Length]);
return DecodeBytes(decoded);
string decodedStr = DecodeBytes(decoded);
if (decodedStr != null && (decodedStr.Contains("Playerjs") || decodedStr.Contains("file:")))
return decodedStr;
try
{
if (rawData.Length > 33)
{
byte state = rawData[0];
byte[] moonKey = new byte[32];
Array.Copy(rawData, 1, moonKey, 0, 32);
int payloadLen = rawData.Length - 33;
byte[] moonPayload = new byte[payloadLen];
Array.Copy(rawData, 33, moonPayload, 0, payloadLen);
for (int i = 0; i < moonPayload.Length; i++)
{
byte encrypted = moonPayload[i];
byte keyByte = moonKey[i % 32];
moonPayload[i] = (byte)(encrypted ^ keyByte ^ state);
state = (byte)((encrypted + keyByte) & 0xFF);
}
string moonDecoded = DecodeBytes(moonPayload);
if (moonDecoded != null && (moonDecoded.Contains("Playerjs") || moonDecoded.Contains("file:")))
return moonDecoded;
}
}
catch { }
return decodedStr;
}
private static byte[] SafeBase64Decode(string value)