From 41ba6dc878c99765738869ba443a2ca5b7d21ba4 Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 30 May 2026 10:09:56 +0300 Subject: [PATCH 1/5] feat(moon_decoder): add moon anime decryption and multi-quality streams --- LME.AnimeON/AnimeONInvoke.cs | 127 +++++++++++++++++++++-- LME.AnimeON/Controller.cs | 55 +++++++++- LME.Mikai/Controller.cs | 53 +++++++++- LME.Mikai/MikaiInvoke.cs | 137 ++++++++++++++++++++++--- LME.Shared/Playerjs/PlayerJsDecoder.cs | 41 +++++++- 5 files changed, 383 insertions(+), 30 deletions(-) diff --git a/LME.AnimeON/AnimeONInvoke.cs b/LME.AnimeON/AnimeONInvoke.cs index 6fb467c..ec3dafd 100644 --- a/LME.AnimeON/AnimeONInvoke.cs +++ b/LME.AnimeON/AnimeONInvoke.cs @@ -169,15 +169,82 @@ namespace LME.AnimeON return JsonSerializer.Deserialize(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 ParseMoonAnimePage(string url) { try { - string requestUrl = $"{url}?player=animeon.club"; + string requestUrl = CleanMoonUrl(url); var headers = new List() { - 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) { diff --git a/LME.AnimeON/Controller.cs b/LME.AnimeON/Controller.cs index a82ed23..322078d 100644 --- a/LME.AnimeON/Controller.cs +++ b/LME.AnimeON/Controller.cs @@ -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, @"\[(?[^\]]+)\](?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() + { + 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() + { + 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")); } diff --git a/LME.Mikai/Controller.cs b/LME.Mikai/Controller.cs index b49ec68..522c18f 100644 --- a/LME.Mikai/Controller.cs +++ b/LME.Mikai/Controller.cs @@ -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, @"\[(?[^\]]+)\](?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() + { + 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() + { + 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")); } diff --git a/LME.Mikai/MikaiInvoke.cs b/LME.Mikai/MikaiInvoke.cs index 8826fb2..27c3c71 100644 --- a/LME.Mikai/MikaiInvoke.cs +++ b/LME.Mikai/MikaiInvoke.cs @@ -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 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() { - 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 ExtractStreamUrls(object filePayload) diff --git a/LME.Shared/Playerjs/PlayerJsDecoder.cs b/LME.Shared/Playerjs/PlayerJsDecoder.cs index 32a1ce3..c84189b 100644 --- a/LME.Shared/Playerjs/PlayerJsDecoder.cs +++ b/LME.Shared/Playerjs/PlayerJsDecoder.cs @@ -11,7 +11,7 @@ namespace LME.Common.Playerjs { public static class PlayerJsDecoder { - private static readonly Regex _reAtobLiteral = new Regex(@"atob\(\s*(['""])(?[A-Za-z0-9+/=]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex _reAtobLiteral = new Regex(@"atob\(\s*(['""])(?[^'""]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex _reJsonParseHelper = new Regex(@"JSON\.parse\(\s*(?[A-Za-z_$][\w$]*)\(\s*(?['""])(?.*?)(\k)\s*\)\s*\)", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); private static readonly Regex _reHelperCall = new Regex(@"^\s*(?[A-Za-z_$][\w$]*)\(\s*(?['""])(?.*?)(\k)\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) From 7216ec8ec7bce8dd624b094cf4a6b9eacb2a13eb Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 30 May 2026 10:13:02 +0300 Subject: [PATCH 2/5] chore(moon_decoder): add missing regex and text imports --- LME.Mikai/Controller.cs | 1 + LME.Mikai/MikaiInvoke.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/LME.Mikai/Controller.cs b/LME.Mikai/Controller.cs index 522c18f..dba0e72 100644 --- a/LME.Mikai/Controller.cs +++ b/LME.Mikai/Controller.cs @@ -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; diff --git a/LME.Mikai/MikaiInvoke.cs b/LME.Mikai/MikaiInvoke.cs index 27c3c71..3ca8cba 100644 --- a/LME.Mikai/MikaiInvoke.cs +++ b/LME.Mikai/MikaiInvoke.cs @@ -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; From 45e4a8171b840cada6d7ff83eb70fb7c2a66b34c Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 30 May 2026 10:17:59 +0300 Subject: [PATCH 3/5] fix(moon_decoder): handle season values as flexible json --- LME.AnimeON/Models/Models.cs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/LME.AnimeON/Models/Models.cs b/LME.AnimeON/Models/Models.cs index 356317c..880c6eb 100644 --- a/LME.AnimeON/Models/Models.cs +++ b/LME.AnimeON/Models/Models.cs @@ -30,7 +30,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 From 352e86857be5c9f7f462b01b7f9d3426716d47c1 Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 30 May 2026 10:18:54 +0300 Subject: [PATCH 4/5] feat: add UAFLIX stream cache fix and update AnimeON models --- LME.AnimeON/Models/Models.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/LME.AnimeON/Models/Models.cs b/LME.AnimeON/Models/Models.cs index 880c6eb..da6bee0 100644 --- a/LME.AnimeON/Models/Models.cs +++ b/LME.AnimeON/Models/Models.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json; using System.Text.Json.Serialization; namespace LME.AnimeON.Models From 3a1e97eefb4b9c5104abab48697b31a76e8b836b Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 30 May 2026 10:21:19 +0300 Subject: [PATCH 5/5] chore(moon_decoder): bump module versions for updated adapters --- LME.AnimeON/ModInit.cs | 2 +- LME.Mikai/ModInit.cs | 2 +- LME.NMoonAnime/ModInit.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LME.AnimeON/ModInit.cs b/LME.AnimeON/ModInit.cs index 88b8056..38e240f 100644 --- a/LME.AnimeON/ModInit.cs +++ b/LME.AnimeON/ModInit.cs @@ -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; diff --git a/LME.Mikai/ModInit.cs b/LME.Mikai/ModInit.cs index 1f27e8a..eb52764 100644 --- a/LME.Mikai/ModInit.cs +++ b/LME.Mikai/ModInit.cs @@ -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; diff --git a/LME.NMoonAnime/ModInit.cs b/LME.NMoonAnime/ModInit.cs index 094db50..ee48552 100644 --- a/LME.NMoonAnime/ModInit.cs +++ b/LME.NMoonAnime/ModInit.cs @@ -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;