Merge pull request #28 from lampame/moon_decoder

Moon decoder
This commit is contained in:
Felix 2026-05-30 10:22:10 +03:00 committed by GitHub
commit 3ee62e24f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 413 additions and 34 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,13 +252,57 @@ 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\('([^']+)'\)");
}
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($"AnimeON ParseMoonAnimePage error: {ex.Message}");

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

@ -26,7 +26,7 @@ namespace LME.AnimeON
{
public class ModInit : IModuleLoaded
{
public static double Version => 4.2;
public static double Version => 4.3;
public static OnlinesSettings AnimeON;
public static bool ApnHostProvided;

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace LME.AnimeON.Models
@ -30,7 +31,30 @@ namespace LME.AnimeON.Models
public string ImdbId { get; set; }
[JsonPropertyName("season")]
public int Season { get; set; }
public System.Text.Json.JsonElement? RawSeason { get; set; }
[JsonIgnore]
public int Season
{
get
{
if (RawSeason == null)
return 0;
var element = RawSeason.Value;
if (element.ValueKind == JsonValueKind.Number && element.TryGetInt32(out int val))
return val;
if (element.ValueKind == JsonValueKind.String)
{
string str = element.GetString();
if (int.TryParse(str, out int val2))
return val2;
}
return 0;
}
}
}
public class FundubsResponseModel

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Mvc;
@ -254,7 +255,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

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
@ -137,22 +138,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 +221,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\('([^']+)'\)");
}
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

@ -25,7 +25,7 @@ namespace LME.Mikai
{
public class ModInit : IModuleLoaded
{
public static double Version => 4.2;
public static double Version => 4.3;
public static OnlinesSettings Mikai;
public static bool ApnHostProvided;

View File

@ -19,7 +19,7 @@ namespace LME.NMoonAnime
{
public class ModInit : IModuleLoaded
{
public static double Version => 2.1;
public static double Version => 2.2;
public static OnlinesSettings NMoonAnime;

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)