using MoonAnime.Models; using Shared; using Shared.Engine; using Shared.Models; using Shared.Models.Online.Settings; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; namespace MoonAnime { public class MoonAnimeInvoke { private readonly OnlinesSettings _init; private readonly IHybridCache _hybridCache; private readonly Action _onLog; private readonly ProxyManager _proxyManager; private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; public MoonAnimeInvoke(OnlinesSettings init, IHybridCache hybridCache, Action onLog, ProxyManager proxyManager) { _init = init; _hybridCache = hybridCache; _onLog = onLog; _proxyManager = proxyManager; } public async Task> Search(string imdbId, string malId, string title, string originalTitle, int year) { string memKey = $"MoonAnime:search:{imdbId}:{malId}:{title}:{originalTitle}:{year}"; if (_hybridCache.TryGetValue(memKey, out List cached)) return cached; try { var endpoints = new[] { "/moonanime/search", "/moonanime" }; foreach (var endpoint in endpoints) { string searchUrl = BuildSearchUrl(endpoint, imdbId, malId, title, originalTitle, year); if (string.IsNullOrWhiteSpace(searchUrl)) continue; _onLog($"MoonAnime: пошук через {searchUrl}"); string json = await Http.Get(_init.cors(searchUrl), headers: DefaultHeaders(), proxy: _proxyManager.Get()); if (string.IsNullOrWhiteSpace(json)) continue; var response = JsonSerializer.Deserialize(json, _jsonOptions); var seasons = response?.Seasons? .Where(s => s != null && !string.IsNullOrWhiteSpace(s.Url)) .Select(s => new MoonAnimeSeasonRef { SeasonNumber = s.SeasonNumber <= 0 ? 1 : s.SeasonNumber, Url = s.Url.Trim() }) .GroupBy(s => s.Url, StringComparer.OrdinalIgnoreCase) .Select(g => g.First()) .OrderBy(s => s.SeasonNumber) .ToList(); if (seasons != null && seasons.Count > 0) { _hybridCache.Set(memKey, seasons, cacheTime(10, init: _init)); return seasons; } } } catch (Exception ex) { _onLog($"MoonAnime: помилка пошуку - {ex.Message}"); } return new List(); } public async Task GetSeasonContent(MoonAnimeSeasonRef season) { if (season == null || string.IsNullOrWhiteSpace(season.Url)) return null; string memKey = $"MoonAnime:season:{season.Url}"; if (_hybridCache.TryGetValue(memKey, out MoonAnimeSeasonContent cached)) return cached; try { _onLog($"MoonAnime: завантаження сезону {season.Url}"); string html = await Http.Get(_init.cors(season.Url), headers: DefaultHeaders(), proxy: _proxyManager.Get()); if (string.IsNullOrWhiteSpace(html)) return null; var content = ParseSeasonPage(html, season.SeasonNumber, season.Url); if (content != null) _hybridCache.Set(memKey, content, cacheTime(20, init: _init)); return content; } catch (Exception ex) { _onLog($"MoonAnime: помилка читання сезону - {ex.Message}"); return null; } } public List ParseStreams(string rawFile) { var streams = new List(); if (string.IsNullOrWhiteSpace(rawFile)) return streams; string value = WebUtility.HtmlDecode(rawFile).Trim(); var bracketMatches = Regex.Matches(value, @"\[(?[^\]]+)\](?https?://[^,\[]+)", RegexOptions.IgnoreCase); foreach (Match match in bracketMatches) { string quality = NormalizeQuality(match.Groups["quality"].Value); string url = match.Groups["url"].Value?.Trim(); if (string.IsNullOrWhiteSpace(url)) continue; streams.Add(new MoonAnimeStreamVariant { Url = url, Quality = quality }); } if (streams.Count == 0) { var taggedMatches = Regex.Matches(value, @"(?\d{3,4}p?)\s*[:|]\s*(?https?://[^,\s]+)", RegexOptions.IgnoreCase); foreach (Match match in taggedMatches) { string quality = NormalizeQuality(match.Groups["quality"].Value); string url = match.Groups["url"].Value?.Trim(); if (string.IsNullOrWhiteSpace(url)) continue; streams.Add(new MoonAnimeStreamVariant { Url = url, Quality = quality }); } } if (streams.Count == 0) { var plainLinks = value .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(part => part.StartsWith("http", StringComparison.OrdinalIgnoreCase)) .ToList(); if (plainLinks.Count > 1) { for (int i = 0; i < plainLinks.Count; i++) { streams.Add(new MoonAnimeStreamVariant { Url = plainLinks[i], Quality = $"auto-{i + 1}" }); } } } if (streams.Count == 0 && value.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { streams.Add(new MoonAnimeStreamVariant { Url = value, Quality = "auto" }); } return streams .Where(s => s != null && !string.IsNullOrWhiteSpace(s.Url)) .Select(s => new MoonAnimeStreamVariant { Url = s.Url.Trim(), Quality = NormalizeQuality(s.Quality) }) .GroupBy(s => s.Url, StringComparer.OrdinalIgnoreCase) .Select(g => g.First()) .OrderByDescending(s => QualityWeight(s.Quality)) .ToList(); } private string BuildSearchUrl(string endpoint, string imdbId, string malId, string title, string originalTitle, int year) { var query = HttpUtility.ParseQueryString(string.Empty); if (!string.IsNullOrWhiteSpace(imdbId)) query["imdb_id"] = imdbId; if (!string.IsNullOrWhiteSpace(malId)) query["mal_id"] = malId; if (!string.IsNullOrWhiteSpace(title)) query["title"] = title; if (!string.IsNullOrWhiteSpace(originalTitle)) query["original_title"] = originalTitle; if (year > 0) query["year"] = year.ToString(); if (query.Count == 0) return null; return $"{_init.apihost.TrimEnd('/')}{endpoint}?{query}"; } private MoonAnimeSeasonContent ParseSeasonPage(string html, int seasonNumber, string seasonUrl) { var content = new MoonAnimeSeasonContent { SeasonNumber = seasonNumber <= 0 ? 1 : seasonNumber, Url = seasonUrl, IsSeries = false }; string fileArrayJson = ExtractFileArrayJson(html); if (string.IsNullOrWhiteSpace(fileArrayJson)) return content; using var doc = JsonDocument.Parse(fileArrayJson); if (doc.RootElement.ValueKind != JsonValueKind.Array) return content; int voiceIndex = 1; foreach (var entry in doc.RootElement.EnumerateArray()) { if (entry.ValueKind != JsonValueKind.Object) continue; var voice = new MoonAnimeVoiceContent { Name = NormalizeVoiceName(GetStringProperty(entry, "title"), voiceIndex) }; if (entry.TryGetProperty("folder", out var folder) && folder.ValueKind == JsonValueKind.Array) { int episodeIndex = 1; foreach (var episodeEntry in folder.EnumerateArray()) { if (episodeEntry.ValueKind != JsonValueKind.Object) continue; string file = GetStringProperty(episodeEntry, "file"); if (string.IsNullOrWhiteSpace(file)) continue; string episodeTitle = GetStringProperty(episodeEntry, "title"); int episodeNumber = ParseEpisodeNumber(episodeTitle, episodeIndex); voice.Episodes.Add(new MoonAnimeEpisodeContent { Name = string.IsNullOrWhiteSpace(episodeTitle) ? $"Епізод {episodeNumber}" : WebUtility.HtmlDecode(episodeTitle), Number = episodeNumber, File = file }); episodeIndex++; } if (voice.Episodes.Count > 0) { content.IsSeries = true; voice.Episodes = voice.Episodes .OrderBy(e => e.Number <= 0 ? int.MaxValue : e.Number) .ThenBy(e => e.Name) .ToList(); } } else { voice.MovieFile = GetStringProperty(entry, "file"); } if (!string.IsNullOrWhiteSpace(voice.MovieFile) || voice.Episodes.Count > 0) content.Voices.Add(voice); voiceIndex++; } return content; } private static string NormalizeVoiceName(string source, int fallbackIndex) { string voice = WebUtility.HtmlDecode(source ?? string.Empty).Trim(); return string.IsNullOrWhiteSpace(voice) ? $"Озвучка {fallbackIndex}" : voice; } private static int ParseEpisodeNumber(string title, int fallback) { if (string.IsNullOrWhiteSpace(title)) return fallback; var match = Regex.Match(title, @"\d+"); if (match.Success && int.TryParse(match.Value, out int number)) return number; return fallback; } private static string NormalizeQuality(string quality) { if (string.IsNullOrWhiteSpace(quality)) return "auto"; string value = quality.Trim().Trim('[', ']'); if (value.Equals("auto", StringComparison.OrdinalIgnoreCase)) return "auto"; var match = Regex.Match(value, @"(?\d{3,4})"); if (match.Success) return $"{match.Groups["q"].Value}p"; return value; } private static int QualityWeight(string quality) { if (string.IsNullOrWhiteSpace(quality)) return 0; var match = Regex.Match(quality, @"\d{3,4}"); if (match.Success && int.TryParse(match.Value, out int q)) return q; return quality.Equals("auto", StringComparison.OrdinalIgnoreCase) ? 1 : 0; } private static string GetStringProperty(JsonElement element, string name) { return element.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String ? value.GetString() : null; } private static string ExtractFileArrayJson(string html) { if (string.IsNullOrWhiteSpace(html)) return null; var match = Regex.Match(html, @"file\s*:\s*(\[[\s\S]*?\])\s*,\s*skip\s*:", RegexOptions.IgnoreCase); if (match.Success) return match.Groups[1].Value; int fileIndex = html.IndexOf("file", StringComparison.OrdinalIgnoreCase); if (fileIndex < 0) return null; int colonIndex = html.IndexOf(':', fileIndex); if (colonIndex < 0) return null; int arrayIndex = html.IndexOf('[', colonIndex); if (arrayIndex < 0) return null; return ExtractBracketArray(html, arrayIndex); } private static string ExtractBracketArray(string source, int startIndex) { bool inString = false; bool escaped = false; char stringChar = '\0'; int depth = 0; int begin = -1; for (int i = startIndex; i < source.Length; i++) { char c = source[i]; if (inString) { if (escaped) { escaped = false; continue; } if (c == '\\') { escaped = true; continue; } if (c == stringChar) { inString = false; stringChar = '\0'; } continue; } if (c == '"' || c == '\'') { inString = true; stringChar = c; continue; } if (c == '[') { if (depth == 0) begin = i; depth++; continue; } if (c == ']') { depth--; if (depth == 0 && begin >= 0) return source.Substring(begin, i - begin + 1); } } return null; } private List DefaultHeaders() { return new List { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }; } 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 = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home; if (ctime > multiaccess) ctime = multiaccess; return TimeSpan.FromMinutes(ctime); } } }