Compare commits

..

No commits in common. "adfa97e810d98cb65fb8275b0c78062063e1e245" and "0f6b0485450ac278bf314a7c3b7111fa4c5483de" have entirely different histories.

13 changed files with 456 additions and 620 deletions

View File

@ -6,13 +6,11 @@ using Shared.Models.Online.Settings;
using Shared.Models;
using Shared.Models.Templates;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Linq;
using System.Text;
using System.Net;
using System.Text.RegularExpressions;
using LME.AnimeON.Models;
using LME.Common.Playerjs;
using Shared.Engine;
namespace LME.AnimeON
@ -185,12 +183,11 @@ namespace LME.AnimeON
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();
var match = System.Text.RegularExpressions.Regex.Match(html, @"file:\s*""([^""]+\.m3u8)""");
if (match.Success)
{
return match.Groups[1].Value;
}
}
catch (Exception ex)
{
@ -200,61 +197,6 @@ namespace LME.AnimeON
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;
}
public async Task<string> ParseAshdiPage(string url, bool disableAshdiMultivoiceForVod = false)
{
var streams = await ParseAshdiPageStreams(url, disableAshdiMultivoiceForVod);

View File

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

View File

@ -7,7 +7,6 @@
"../LME.Shared/GlobalUsings.cs",
"../LME.Shared/Online/OnlineRegistry.cs",
"../LME.Shared/Update/ModuleUpdateService.cs",
"../LME.Shared/Apn/ApnHelper.cs",
"../LME.Shared/Playerjs/PlayerJsDecoder.cs"
"../LME.Shared/Apn/ApnHelper.cs"
]
}

View File

@ -95,15 +95,9 @@ namespace LME.JackTor.Controllers
var seasonTpl = new SeasonTpl(quality: quality);
foreach (int season in seasons)
{
int seasonYear = torrents
.Where(i => i.Seasons != null && i.Seasons.Contains(season) && i.ExtractedYear > 1900)
.Select(i => i.ExtractedYear)
.DefaultIfEmpty(year)
.Min();
seasonTpl.Append(
$"{season} сезон",
$"{host}/lite/lme_jacktor?rjson={rjson}&title={enTitle}&original_title={enOriginal}&year={seasonYear}&original_language={original_language}&serial=1&s={season}",
$"{host}/lite/lme_jacktor?rjson={rjson}&title={enTitle}&original_title={enOriginal}&year={year}&original_language={original_language}&serial=1&s={season}",
season);
}

View File

@ -335,7 +335,7 @@ namespace LME.JackTor
continue;
int extractedYear = ExtractYear(searchable);
if (serial != 1 && year > 1900 && extractedYear > 1900 && Math.Abs(extractedYear - year) > yearTolerance)
if (year > 1900 && extractedYear > 1900 && Math.Abs(extractedYear - year) > yearTolerance)
continue;
int[] seasons = ParseSeasons(searchable);

View File

@ -18,7 +18,7 @@ namespace LME.JackTor
{
public class ModInit : IModuleLoaded
{
public static double Version => 2.2;
public static double Version => 2.1;
public static JackTorSettings JackTor;

View File

@ -2,13 +2,11 @@ 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 LME.Common.Playerjs;
using Shared;
using Shared.Engine;
using Shared.Models;
@ -160,73 +158,16 @@ namespace LME.Mikai
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();
var match = System.Text.RegularExpressions.Regex.Match(html, @"file:\s*""([^""]+\.m3u8)""");
if (match.Success)
return match.Groups[1].Value;
}
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;
return null;
}
string AshdiRequestUrl(string url)

View File

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

View File

@ -7,7 +7,6 @@
"../LME.Shared/GlobalUsings.cs",
"../LME.Shared/Online/OnlineRegistry.cs",
"../LME.Shared/Update/ModuleUpdateService.cs",
"../LME.Shared/Apn/ApnHelper.cs",
"../LME.Shared/Playerjs/PlayerJsDecoder.cs"
"../LME.Shared/Apn/ApnHelper.cs"
]
}

View File

@ -1,5 +1,4 @@
using LME.NMoonAnime.Models;
using LME.Common.Playerjs;
using Shared;
using Shared.Engine;
using Shared.Models;
@ -30,6 +29,12 @@ namespace LME.NMoonAnime
};
private static readonly Regex _reSeason = new Regex(@"(?:season|сезон)\s*(\d+)|(\d+)\s*(?:season|сезон)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex _reEpisode = new Regex(@"(?:episode|серія|серия|епізод|ep)\s*(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex _reTrailingComma = new Regex(@",\s*([}\]])", RegexOptions.Compiled);
private static readonly Regex _reAtobLiteral = new Regex(@"atob\(\s*(['""])(?<payload>[A-Za-z0-9+/=]+)\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 UTF8Encoding _utf8Strict = new UTF8Encoding(false, true);
private static readonly Encoding _latin1 = Encoding.GetEncoding("ISO-8859-1");
public NMoonAnimeInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
@ -236,7 +241,7 @@ namespace LME.NMoonAnime
IsSeries = false
};
var payload = PlayerJsDecoder.ExtractPlayerPayload(html);
var payload = ExtractPlayerPayload(html);
if (payload == null)
return content;
@ -490,14 +495,437 @@ namespace LME.NMoonAnime
return entries;
}
private PlayerPayload ExtractPlayerPayload(string htmlText)
{
string cleanHtml = WebUtility.HtmlDecode(htmlText ?? string.Empty);
if (string.IsNullOrWhiteSpace(cleanHtml))
return null;
var candidates = new List<string> { cleanHtml };
string decodedScript = DecodeOuterPlayerScript(cleanHtml);
if (!string.IsNullOrWhiteSpace(decodedScript))
candidates.Insert(0, decodedScript);
foreach (string sourceText in candidates)
{
string objectText = ExtractObjectByBraces(sourceText, "new Playerjs");
if (string.IsNullOrWhiteSpace(objectText))
objectText = ExtractObjectByBraces(sourceText, "Playerjs({");
string searchText = string.IsNullOrWhiteSpace(objectText) ? sourceText : objectText;
string fileValue = ExtractJsValue(searchText, "file");
if (fileValue == null && !string.IsNullOrWhiteSpace(objectText))
fileValue = ExtractJsValue(sourceText, "file");
if (fileValue == null)
continue;
string titleValue = ExtractJsValue(searchText, "title");
object parsedFile = ParsePlayerFileValue(fileValue, sourceText);
return new PlayerPayload
{
Title = Nullish(titleValue),
FilePayload = parsedFile
};
}
return null;
}
private object ParsePlayerFileValue(string rawValue, string contextText)
{
string text = rawValue?.Trim();
if (string.IsNullOrWhiteSpace(text))
return rawValue;
if (text.StartsWith("[") || text.StartsWith("{"))
{
JsonNode loaded = LoadJsonLoose(text);
if (loaded != null)
return loaded;
}
var parseMatch = _reJsonParseHelper.Match(text);
if (parseMatch.Success)
{
string decoded = DecodeHelperPayload(parseMatch.Groups["fn"].Value, parseMatch.Groups["payload"].Value, contextText);
if (!string.IsNullOrWhiteSpace(decoded))
{
JsonNode loaded = LoadJsonLoose(decoded);
if (loaded != null)
return loaded;
}
}
var helperMatch = _reHelperCall.Match(text);
if (helperMatch.Success)
{
string decoded = DecodeHelperPayload(helperMatch.Groups["fn"].Value, helperMatch.Groups["payload"].Value, contextText);
if (!string.IsNullOrWhiteSpace(decoded))
{
JsonNode loaded = LoadJsonLoose(decoded);
if (loaded != null)
return loaded;
return decoded;
}
}
return rawValue;
}
private string DecodeHelperPayload(string helperName, string payload, string contextText)
{
if (string.IsNullOrWhiteSpace(helperName))
return null;
if (helperName.Equals("atob", StringComparison.OrdinalIgnoreCase))
{
byte[] rawBytes = SafeBase64Decode(payload);
return rawBytes == null ? null : DecodeBytes(rawBytes);
}
string helperKey = ExtractHelperKey(contextText, helperName);
if (string.IsNullOrWhiteSpace(helperKey))
return null;
byte[] keyBytes = Encoding.UTF8.GetBytes(helperKey);
if (keyBytes.Length == 0)
return null;
byte[] payloadBytes = SafeBase64Decode(payload);
if (payloadBytes == null)
return null;
var decoded = new byte[payloadBytes.Length];
for (int index = 0; index < payloadBytes.Length; index++)
decoded[index] = (byte)(payloadBytes[index] ^ keyBytes[index % keyBytes.Length]);
return DecodeBytes(decoded);
}
private static string ExtractHelperKey(string contextText, string helperName)
{
if (string.IsNullOrWhiteSpace(contextText) || string.IsNullOrWhiteSpace(helperName))
return null;
string pattern = $@"function\s+{Regex.Escape(helperName)}\s*\([^)]*\)\s*\{{[\s\S]*?var\s+k\s*=\s*(['""])(?<key>.*?)\1";
var match = Regex.Match(contextText, pattern, RegexOptions.IgnoreCase);
if (!match.Success)
return null;
return Nullish(match.Groups["key"].Value);
}
private static string DecodeOuterPlayerScript(string text)
{
if (string.IsNullOrWhiteSpace(text))
return null;
var match = _reAtobLiteral.Match(text);
if (!match.Success)
return null;
byte[] rawData = SafeBase64Decode(match.Groups["payload"].Value);
if (rawData == null || rawData.Length <= 32)
return null;
byte[] key = rawData.Take(32).ToArray();
byte[] encryptedData = rawData.Skip(32).ToArray();
var decoded = new byte[encryptedData.Length];
for (int index = 0; index < encryptedData.Length; index++)
decoded[index] = (byte)(encryptedData[index] ^ key[index % key.Length]);
return DecodeBytes(decoded);
}
private static byte[] SafeBase64Decode(string value)
{
string text = value?.Trim();
if (string.IsNullOrWhiteSpace(text))
return null;
int remainder = text.Length % 4;
if (remainder != 0)
text += new string('=', 4 - remainder);
try
{
return Convert.FromBase64String(text);
}
catch
{
return null;
}
}
private static string DecodeBytes(byte[] data)
{
if (data == null || data.Length == 0)
return null;
try
{
return _utf8Strict.GetString(data);
}
catch
{
return _latin1.GetString(data);
}
}
private static string ExtractObjectByBraces(string text, string anchor)
{
if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(anchor))
return null;
int anchorIndex = text.IndexOf(anchor, StringComparison.OrdinalIgnoreCase);
if (anchorIndex < 0)
return null;
int braceIndex = text.IndexOf('{', anchorIndex);
if (braceIndex < 0)
return null;
int depth = 0;
bool escaped = false;
char? inString = null;
for (int index = braceIndex; index < text.Length; index++)
{
char current = text[index];
if (inString.HasValue)
{
if (escaped)
{
escaped = false;
continue;
}
if (current == '\\')
{
escaped = true;
continue;
}
if (current == inString.Value)
inString = null;
continue;
}
if (current == '"' || current == '\'')
{
inString = current;
continue;
}
if (current == '{')
{
depth++;
continue;
}
if (current == '}')
{
depth--;
if (depth == 0)
return text.Substring(braceIndex + 1, index - braceIndex - 1);
}
}
return null;
}
private static string ExtractJsValue(string text, string key)
{
if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(key))
return null;
var match = Regex.Match(text, $@"\b{Regex.Escape(key)}\b\s*:\s*", RegexOptions.IgnoreCase);
if (!match.Success)
return null;
int index = match.Index + match.Length;
while (index < text.Length && char.IsWhiteSpace(text[index]))
index++;
if (index >= text.Length)
return null;
char token = text[index];
if (token == '"' || token == '\'')
{
var (value, _) = ReadJsString(text, index);
return value;
}
if (token == '[')
{
int endIndex = FindMatchingBracket(text, index, '[', ']');
return endIndex >= index ? text.Substring(index, endIndex - index + 1) : null;
}
if (token == '{')
{
int endIndex = FindMatchingBracket(text, index, '{', '}');
return endIndex >= index ? text.Substring(index, endIndex - index + 1) : null;
}
int stopIndex = index;
while (stopIndex < text.Length && text[stopIndex] != ',' && text[stopIndex] != '}' && text[stopIndex] != '\n' && text[stopIndex] != '\r')
stopIndex++;
return text.Substring(index, stopIndex - index).Trim();
}
private static (string value, int nextIndex) ReadJsString(string text, int startIndex)
{
if (string.IsNullOrWhiteSpace(text) || startIndex < 0 || startIndex >= text.Length)
return (null, -1);
char quote = text[startIndex];
if (quote != '"' && quote != '\'')
return (null, -1);
var buffer = new StringBuilder();
bool escaped = false;
for (int index = startIndex + 1; index < text.Length; index++)
{
char current = text[index];
if (escaped)
{
buffer.Append(current);
escaped = false;
continue;
}
if (current == '\\')
{
escaped = true;
continue;
}
if (current == quote)
return (buffer.ToString(), index + 1);
buffer.Append(current);
}
return (null, -1);
}
private static int FindMatchingBracket(string text, int startIndex, char openChar, char closeChar)
{
if (string.IsNullOrWhiteSpace(text) || startIndex < 0 || startIndex >= text.Length || text[startIndex] != openChar)
return -1;
int depth = 0;
bool escaped = false;
char? inString = null;
for (int index = startIndex; index < text.Length; index++)
{
char current = text[index];
if (inString.HasValue)
{
if (escaped)
{
escaped = false;
continue;
}
if (current == '\\')
{
escaped = true;
continue;
}
if (current == inString.Value)
inString = null;
continue;
}
if (current == '"' || current == '\'')
{
inString = current;
continue;
}
if (current == openChar)
{
depth++;
continue;
}
if (current == closeChar)
{
depth--;
if (depth == 0)
return index;
}
}
return -1;
}
private static JsonNode LoadJsonLoose(string value)
{
return PlayerJsDecoder.LoadJsonLoose(value);
string text = value?.Trim();
if (string.IsNullOrWhiteSpace(text))
return null;
string normalized = WebUtility.HtmlDecode(text).Replace("\\/", "/");
string unescapedQuotes = normalized.Replace("\\'", "'").Replace("\\\"", "\"");
var candidates = new[]
{
normalized,
unescapedQuotes,
RemoveTrailingCommas(normalized),
RemoveTrailingCommas(unescapedQuotes)
};
foreach (string candidate in candidates.Distinct(StringComparer.Ordinal))
{
if (string.IsNullOrWhiteSpace(candidate))
continue;
try
{
return JsonNode.Parse(candidate);
}
catch
{
}
}
return null;
}
private static string RemoveTrailingCommas(string value)
{
return string.IsNullOrWhiteSpace(value) ? value : _reTrailingComma.Replace(value, "$1");
}
private static string Nullish(string value)
{
return PlayerJsDecoder.Nullish(value);
string text = value?.Trim();
if (string.IsNullOrWhiteSpace(text))
return null;
if (text.Equals("null", StringComparison.OrdinalIgnoreCase) ||
text.Equals("none", StringComparison.OrdinalIgnoreCase) ||
text.Equals("undefined", StringComparison.OrdinalIgnoreCase))
return null;
return text;
}
private static bool TryGetArray(JsonObject obj, string key, out JsonArray array)
@ -692,6 +1120,13 @@ namespace LME.NMoonAnime
return TimeSpan.FromMinutes(ctime);
}
private sealed class PlayerPayload
{
public string Title { get; set; }
public object FilePayload { get; set; }
}
private sealed class NMoonAnimeMovieEntry
{
public string Title { get; set; }

View File

@ -7,7 +7,6 @@
"../LME.Shared/GlobalUsings.cs",
"../LME.Shared/Online/OnlineRegistry.cs",
"../LME.Shared/Update/ModuleUpdateService.cs",
"../LME.Shared/Apn/ApnHelper.cs",
"../LME.Shared/Playerjs/PlayerJsDecoder.cs"
"../LME.Shared/Apn/ApnHelper.cs"
]
}

View File

@ -1,12 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Shared">
<HintPath>..\..\Shared.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -1,461 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
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 _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);
private static readonly UTF8Encoding _utf8Strict = new UTF8Encoding(false, true);
private static readonly Encoding _latin1 = Encoding.GetEncoding("ISO-8859-1");
public static PlayerPayload ExtractPlayerPayload(string htmlText)
{
string cleanHtml = WebUtility.HtmlDecode(htmlText ?? string.Empty);
if (string.IsNullOrWhiteSpace(cleanHtml))
return null;
var candidates = new List<string> { cleanHtml };
string decodedScript = DecodeOuterPlayerScript(cleanHtml);
if (!string.IsNullOrWhiteSpace(decodedScript))
candidates.Insert(0, decodedScript);
foreach (string sourceText in candidates)
{
string objectText = ExtractObjectByBraces(sourceText, "new Playerjs");
if (string.IsNullOrWhiteSpace(objectText))
objectText = ExtractObjectByBraces(sourceText, "Playerjs({");
string searchText = string.IsNullOrWhiteSpace(objectText) ? sourceText : objectText;
string fileValue = ExtractJsValue(searchText, "file");
if (fileValue == null && !string.IsNullOrWhiteSpace(objectText))
fileValue = ExtractJsValue(sourceText, "file");
if (fileValue == null)
continue;
string titleValue = ExtractJsValue(searchText, "title");
object parsedFile = ParsePlayerFileValue(fileValue, sourceText);
return new PlayerPayload
{
Title = Nullish(titleValue),
FilePayload = parsedFile
};
}
return null;
}
private static object ParsePlayerFileValue(string rawValue, string contextText)
{
string text = rawValue?.Trim();
if (string.IsNullOrWhiteSpace(text))
return rawValue;
if (text.StartsWith("[") || text.StartsWith("{"))
{
JsonNode loaded = LoadJsonLoose(text);
if (loaded != null)
return loaded;
}
var parseMatch = _reJsonParseHelper.Match(text);
if (parseMatch.Success)
{
string decoded = DecodeHelperPayload(parseMatch.Groups["fn"].Value, parseMatch.Groups["payload"].Value, contextText);
if (!string.IsNullOrWhiteSpace(decoded))
{
JsonNode loaded = LoadJsonLoose(decoded);
if (loaded != null)
return loaded;
}
}
var helperMatch = _reHelperCall.Match(text);
if (helperMatch.Success)
{
string decoded = DecodeHelperPayload(helperMatch.Groups["fn"].Value, helperMatch.Groups["payload"].Value, contextText);
if (!string.IsNullOrWhiteSpace(decoded))
{
JsonNode loaded = LoadJsonLoose(decoded);
if (loaded != null)
return loaded;
return decoded;
}
}
return rawValue;
}
private static string DecodeHelperPayload(string helperName, string payload, string contextText)
{
if (string.IsNullOrWhiteSpace(helperName))
return null;
if (helperName.Equals("atob", StringComparison.OrdinalIgnoreCase))
{
byte[] rawBytes = SafeBase64Decode(payload);
return rawBytes == null ? null : DecodeBytes(rawBytes);
}
string helperKey = ExtractHelperKey(contextText, helperName);
if (string.IsNullOrWhiteSpace(helperKey))
return null;
byte[] keyBytes = Encoding.UTF8.GetBytes(helperKey);
if (keyBytes.Length == 0)
return null;
byte[] payloadBytes = SafeBase64Decode(payload);
if (payloadBytes == null)
return null;
var decoded = new byte[payloadBytes.Length];
for (int index = 0; index < payloadBytes.Length; index++)
decoded[index] = (byte)(payloadBytes[index] ^ keyBytes[index % keyBytes.Length]);
return DecodeBytes(decoded);
}
private static string ExtractHelperKey(string contextText, string helperName)
{
if (string.IsNullOrWhiteSpace(contextText) || string.IsNullOrWhiteSpace(helperName))
return null;
string pattern = $@"function\s+{Regex.Escape(helperName)}\s*\([^)]*\)\s*\{{[\s\S]*?var\s+k\s*=\s*(['""])(?<key>.*?)\1";
var match = Regex.Match(contextText, pattern, RegexOptions.IgnoreCase);
if (!match.Success)
return null;
return Nullish(match.Groups["key"].Value);
}
private static string DecodeOuterPlayerScript(string text)
{
if (string.IsNullOrWhiteSpace(text))
return null;
var match = _reAtobLiteral.Match(text);
if (!match.Success)
return null;
byte[] rawData = SafeBase64Decode(match.Groups["payload"].Value);
if (rawData == null || rawData.Length <= 32)
return null;
byte[] key = rawData.Take(32).ToArray();
byte[] encryptedData = rawData.Skip(32).ToArray();
var decoded = new byte[encryptedData.Length];
for (int index = 0; index < encryptedData.Length; index++)
decoded[index] = (byte)(encryptedData[index] ^ key[index % key.Length]);
return DecodeBytes(decoded);
}
private static byte[] SafeBase64Decode(string value)
{
string text = value?.Trim();
if (string.IsNullOrWhiteSpace(text))
return null;
int remainder = text.Length % 4;
if (remainder != 0)
text += new string('=', 4 - remainder);
try
{
return Convert.FromBase64String(text);
}
catch
{
return null;
}
}
private static string DecodeBytes(byte[] data)
{
if (data == null || data.Length == 0)
return null;
try
{
return _utf8Strict.GetString(data);
}
catch
{
return _latin1.GetString(data);
}
}
private static string ExtractObjectByBraces(string text, string anchor)
{
if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(anchor))
return null;
int anchorIndex = text.IndexOf(anchor, StringComparison.OrdinalIgnoreCase);
if (anchorIndex < 0)
return null;
int braceIndex = text.IndexOf('{', anchorIndex);
if (braceIndex < 0)
return null;
int depth = 0;
bool escaped = false;
char? inString = null;
for (int index = braceIndex; index < text.Length; index++)
{
char current = text[index];
if (inString.HasValue)
{
if (escaped)
{
escaped = false;
continue;
}
if (current == '\\')
{
escaped = true;
continue;
}
if (current == inString.Value)
inString = null;
continue;
}
if (current == '"' || current == '\'')
{
inString = current;
continue;
}
if (current == '{')
{
depth++;
continue;
}
if (current == '}')
{
depth--;
if (depth == 0)
return text.Substring(braceIndex + 1, index - braceIndex - 1);
}
}
return null;
}
private static string ExtractJsValue(string text, string key)
{
if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(key))
return null;
var match = Regex.Match(text, $@"\b{Regex.Escape(key)}\b\s*:\s*", RegexOptions.IgnoreCase);
if (!match.Success)
return null;
int index = match.Index + match.Length;
while (index < text.Length && char.IsWhiteSpace(text[index]))
index++;
if (index >= text.Length)
return null;
char token = text[index];
if (token == '"' || token == '\'')
{
var (value, _) = ReadJsString(text, index);
return value;
}
if (token == '[')
{
int endIndex = FindMatchingBracket(text, index, '[', ']');
return endIndex >= index ? text.Substring(index, endIndex - index + 1) : null;
}
if (token == '{')
{
int endIndex = FindMatchingBracket(text, index, '{', '}');
return endIndex >= index ? text.Substring(index, endIndex - index + 1) : null;
}
int stopIndex = index;
while (stopIndex < text.Length && text[stopIndex] != ',' && text[stopIndex] != '}' && text[stopIndex] != '\n' && text[stopIndex] != '\r')
stopIndex++;
return text.Substring(index, stopIndex - index).Trim();
}
private static (string value, int nextIndex) ReadJsString(string text, int startIndex)
{
if (string.IsNullOrWhiteSpace(text) || startIndex < 0 || startIndex >= text.Length)
return (null, -1);
char quote = text[startIndex];
if (quote != '"' && quote != '\'')
return (null, -1);
var buffer = new StringBuilder();
bool escaped = false;
for (int index = startIndex + 1; index < text.Length; index++)
{
char current = text[index];
if (escaped)
{
buffer.Append(current);
escaped = false;
continue;
}
if (current == '\\')
{
escaped = true;
continue;
}
if (current == quote)
return (buffer.ToString(), index + 1);
buffer.Append(current);
}
return (null, -1);
}
private static int FindMatchingBracket(string text, int startIndex, char openChar, char closeChar)
{
if (string.IsNullOrWhiteSpace(text) || startIndex < 0 || startIndex >= text.Length || text[startIndex] != openChar)
return -1;
int depth = 0;
bool escaped = false;
char? inString = null;
for (int index = startIndex; index < text.Length; index++)
{
char current = text[index];
if (inString.HasValue)
{
if (escaped)
{
escaped = false;
continue;
}
if (current == '\\')
{
escaped = true;
continue;
}
if (current == inString.Value)
inString = null;
continue;
}
if (current == '"' || current == '\'')
{
inString = current;
continue;
}
if (current == openChar)
{
depth++;
continue;
}
if (current == closeChar)
{
depth--;
if (depth == 0)
return index;
}
}
return -1;
}
public static JsonNode LoadJsonLoose(string value)
{
string text = value?.Trim();
if (string.IsNullOrWhiteSpace(text))
return null;
string normalized = WebUtility.HtmlDecode(text).Replace("\\/", "/");
string unescapedQuotes = normalized.Replace("\\'", "'").Replace("\\\"", "\"");
var candidates = new[]
{
normalized,
unescapedQuotes,
RemoveTrailingCommas(normalized),
RemoveTrailingCommas(unescapedQuotes)
};
foreach (string candidate in candidates.Distinct(StringComparer.Ordinal))
{
if (string.IsNullOrWhiteSpace(candidate))
continue;
try
{
return JsonNode.Parse(candidate);
}
catch
{
}
}
return null;
}
private static string RemoveTrailingCommas(string value)
{
return string.IsNullOrWhiteSpace(value) ? value : _reTrailingComma.Replace(value, "$1");
}
public static string Nullish(string value)
{
string text = value?.Trim();
if (string.IsNullOrWhiteSpace(text))
return null;
if (text.Equals("null", StringComparison.OrdinalIgnoreCase) ||
text.Equals("none", StringComparison.OrdinalIgnoreCase) ||
text.Equals("undefined", StringComparison.OrdinalIgnoreCase))
return null;
return text;
}
}
public sealed class PlayerPayload
{
public string Title { get; set; }
public object FilePayload { get; set; }
}
}