From bffdf68bde2a0f0d0c9fbdb7ada1c59867d50eec Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 7 Mar 2026 17:02:16 +0200 Subject: [PATCH 1/5] feat(docs): add MoonAnime source to README documentation --- MoonAnime/ApnHelper.cs | 86 ++++++ MoonAnime/Controller.cs | 342 ++++++++++++++++++++ MoonAnime/ModInit.cs | 194 ++++++++++++ MoonAnime/Models/MoonAnimeModels.cs | 56 ++++ MoonAnime/MoonAnime.csproj | 15 + MoonAnime/MoonAnimeInvoke.cs | 462 ++++++++++++++++++++++++++++ MoonAnime/OnlineApi.cs | 44 +++ MoonAnime/manifest.json | 6 + README.md | 3 + 9 files changed, 1208 insertions(+) create mode 100644 MoonAnime/ApnHelper.cs create mode 100644 MoonAnime/Controller.cs create mode 100644 MoonAnime/ModInit.cs create mode 100644 MoonAnime/Models/MoonAnimeModels.cs create mode 100644 MoonAnime/MoonAnime.csproj create mode 100644 MoonAnime/MoonAnimeInvoke.cs create mode 100644 MoonAnime/OnlineApi.cs create mode 100644 MoonAnime/manifest.json diff --git a/MoonAnime/ApnHelper.cs b/MoonAnime/ApnHelper.cs new file mode 100644 index 0000000..394a5bc --- /dev/null +++ b/MoonAnime/ApnHelper.cs @@ -0,0 +1,86 @@ +using Newtonsoft.Json.Linq; +using Shared.Models.Base; +using System; +using System.Web; + +namespace Shared.Engine +{ + public static class ApnHelper + { + public const string DefaultHost = "https://tut.im/proxy.php?url={encodeurl}"; + + public static bool TryGetInitConf(JObject conf, out bool enabled, out string host) + { + enabled = false; + host = null; + + if (conf == null) + return false; + + if (!conf.TryGetValue("apn", out var apnToken) || apnToken?.Type != JTokenType.Boolean) + return false; + + enabled = apnToken.Value(); + host = conf.Value("apn_host"); + return true; + } + + public static void ApplyInitConf(bool enabled, string host, BaseSettings init) + { + if (init == null) + return; + + if (!enabled) + { + init.apnstream = false; + init.apn = null; + return; + } + + if (string.IsNullOrWhiteSpace(host)) + host = DefaultHost; + + if (init.apn == null) + init.apn = new ApnConf(); + + init.apn.host = host; + init.apnstream = true; + } + + public static bool IsEnabled(BaseSettings init) + { + return init?.apnstream == true && !string.IsNullOrWhiteSpace(init?.apn?.host); + } + + public static bool IsAshdiUrl(string url) + { + return !string.IsNullOrEmpty(url) && + url.IndexOf("ashdi.vip", StringComparison.OrdinalIgnoreCase) >= 0; + } + + public static string WrapUrl(BaseSettings init, string url) + { + if (!IsEnabled(init)) + return url; + + return BuildUrl(init.apn.host, url); + } + + public static string BuildUrl(string host, string url) + { + if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(url)) + return url; + + if (host.Contains("{encodeurl}")) + return host.Replace("{encodeurl}", HttpUtility.UrlEncode(url)); + + if (host.Contains("{encode_uri}")) + return host.Replace("{encode_uri}", HttpUtility.UrlEncode(url)); + + if (host.Contains("{uri}")) + return host.Replace("{uri}", url); + + return $"{host.TrimEnd('/')}/{url}"; + } + } +} diff --git a/MoonAnime/Controller.cs b/MoonAnime/Controller.cs new file mode 100644 index 0000000..dd13a44 --- /dev/null +++ b/MoonAnime/Controller.cs @@ -0,0 +1,342 @@ +using Microsoft.AspNetCore.Mvc; +using MoonAnime.Models; +using Shared; +using Shared.Engine; +using Shared.Models; +using Shared.Models.Online.Settings; +using Shared.Models.Templates; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web; + +namespace MoonAnime.Controllers +{ + public class Controller : BaseOnlineController + { + private readonly ProxyManager proxyManager; + + public Controller() : base(ModInit.Settings) + { + proxyManager = new ProxyManager(ModInit.MoonAnime); + } + + [HttpGet] + [Route("moonanime")] + public async Task Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string mal_id, string t, int s = -1, bool rjson = false, bool checksearch = false) + { + await UpdateService.ConnectAsync(host); + + var init = await loadKit(ModInit.MoonAnime); + if (!init.enable) + return Forbid(); + + var invoke = new MoonAnimeInvoke(init, hybridCache, OnLog, proxyManager); + + if (checksearch) + { + if (AppInit.conf?.online?.checkOnlineSearch != true) + return OnError("moonanime", proxyManager); + + var checkResults = await invoke.Search(imdb_id, mal_id, title, original_title, year); + if (checkResults != null && checkResults.Count > 0) + return Content("data-json=", "text/plain; charset=utf-8"); + + return OnError("moonanime", proxyManager); + } + + OnLog($"MoonAnime: title={title}, original_title={original_title}, imdb={imdb_id}, mal_id={mal_id}, serial={serial}, s={s}, t={t}"); + + var seasons = await invoke.Search(imdb_id, mal_id, title, original_title, year); + if (seasons == null || seasons.Count == 0) + return OnError("moonanime", proxyManager); + + bool isSeries = serial == 1; + MoonAnimeSeasonContent firstSeasonData = null; + + if (serial == -1) + { + firstSeasonData = await invoke.GetSeasonContent(seasons[0]); + if (firstSeasonData == null || firstSeasonData.Voices.Count == 0) + return OnError("moonanime", proxyManager); + + isSeries = firstSeasonData.IsSeries; + } + + if (isSeries) + { + return await RenderSerial(invoke, seasons, imdb_id, kinopoisk_id, title, original_title, year, mal_id, s, t, rjson); + } + + return await RenderMovie(invoke, seasons, title, original_title, firstSeasonData, rjson); + } + + [HttpGet("moonanime/play")] + public async Task Play(string file, string title = null) + { + await UpdateService.ConnectAsync(host); + + var init = await loadKit(ModInit.MoonAnime); + if (!init.enable) + return Forbid(); + + if (string.IsNullOrWhiteSpace(file)) + return OnError("moonanime", proxyManager); + + var invoke = new MoonAnimeInvoke(init, hybridCache, OnLog, proxyManager); + var streams = invoke.ParseStreams(file); + if (streams == null || streams.Count == 0) + return OnError("moonanime", proxyManager); + + if (streams.Count == 1) + { + string singleUrl = BuildStreamUrl(init, streams[0].Url); + string singleJson = VideoTpl.ToJson("play", singleUrl, title ?? string.Empty, quality: streams[0].Quality ?? "auto"); + return UpdateService.Validate(Content(singleJson, "application/json; charset=utf-8")); + } + + var streamQuality = new StreamQualityTpl(); + foreach (var stream in streams) + { + string streamUrl = BuildStreamUrl(init, stream.Url); + streamQuality.Append(streamUrl, stream.Quality); + } + + if (!streamQuality.Any()) + return OnError("moonanime", proxyManager); + + var first = streamQuality.Firts(); + string json = VideoTpl.ToJson("play", first.link, title ?? string.Empty, streamquality: streamQuality); + return UpdateService.Validate(Content(json, "application/json; charset=utf-8")); + } + + private async Task RenderSerial( + MoonAnimeInvoke invoke, + List seasons, + string imdbId, + long kinopoiskId, + string title, + string originalTitle, + int year, + string malId, + int selectedSeason, + string selectedVoice, + bool rjson) + { + var orderedSeasons = seasons + .Where(s => s != null && !string.IsNullOrWhiteSpace(s.Url)) + .OrderBy(s => s.SeasonNumber) + .ToList(); + + if (orderedSeasons.Count == 0) + return OnError("moonanime", proxyManager); + + if (selectedSeason == -1) + { + var seasonTpl = new SeasonTpl(orderedSeasons.Count); + foreach (var season in orderedSeasons) + { + int seasonNumber = season.SeasonNumber <= 0 ? 1 : season.SeasonNumber; + string seasonName = $"Сезон {seasonNumber}"; + string seasonLink = BuildIndexUrl(imdbId, kinopoiskId, title, originalTitle, year, 1, malId, seasonNumber, selectedVoice); + seasonTpl.Append(seasonName, seasonLink, seasonNumber); + } + + return rjson + ? Content(seasonTpl.ToJson(), "application/json; charset=utf-8") + : Content(seasonTpl.ToHtml(), "text/html; charset=utf-8"); + } + + var currentSeason = orderedSeasons.FirstOrDefault(s => s.SeasonNumber == selectedSeason) ?? orderedSeasons[0]; + var seasonData = await invoke.GetSeasonContent(currentSeason); + if (seasonData == null) + return OnError("moonanime", proxyManager); + + var voices = seasonData.Voices + .Where(v => v != null && v.Episodes != null && v.Episodes.Count > 0) + .ToList(); + + if (voices.Count == 0) + return OnError("moonanime", proxyManager); + + int activeVoiceIndex = ParseVoiceIndex(selectedVoice, voices.Count); + var voiceTpl = new VoiceTpl(voices.Count); + for (int i = 0; i < voices.Count; i++) + { + string voiceName = string.IsNullOrWhiteSpace(voices[i].Name) ? $"Озвучка {i + 1}" : voices[i].Name; + string voiceLink = BuildIndexUrl(imdbId, kinopoiskId, title, originalTitle, year, 1, malId, currentSeason.SeasonNumber, i.ToString()); + voiceTpl.Append(voiceName, i == activeVoiceIndex, voiceLink); + } + + var selectedVoiceData = voices[activeVoiceIndex]; + var episodes = selectedVoiceData.Episodes + .Where(e => e != null && !string.IsNullOrWhiteSpace(e.File)) + .OrderBy(e => e.Number <= 0 ? int.MaxValue : e.Number) + .ThenBy(e => e.Name) + .ToList(); + + if (episodes.Count == 0) + return OnError("moonanime", proxyManager); + + string displayTitle = !string.IsNullOrWhiteSpace(title) + ? title + : !string.IsNullOrWhiteSpace(originalTitle) + ? originalTitle + : "MoonAnime"; + + var episodeTpl = new EpisodeTpl(episodes.Count); + foreach (var episode in episodes) + { + int episodeNumber = episode.Number <= 0 ? 1 : episode.Number; + string episodeName = string.IsNullOrWhiteSpace(episode.Name) ? $"Епізод {episodeNumber}" : episode.Name; + string callUrl = $"{host}/moonanime/play?file={HttpUtility.UrlEncode(episode.File)}&title={HttpUtility.UrlEncode(displayTitle)}"; + episodeTpl.Append(episodeName, displayTitle, currentSeason.SeasonNumber.ToString(), episodeNumber.ToString(), accsArgs(callUrl), "call"); + } + + episodeTpl.Append(voiceTpl); + + return rjson + ? Content(episodeTpl.ToJson(), "application/json; charset=utf-8") + : Content(episodeTpl.ToHtml(), "text/html; charset=utf-8"); + } + + private async Task RenderMovie( + MoonAnimeInvoke invoke, + List seasons, + string title, + string originalTitle, + MoonAnimeSeasonContent firstSeasonData, + bool rjson) + { + var currentSeason = seasons + .Where(s => s != null && !string.IsNullOrWhiteSpace(s.Url)) + .OrderBy(s => s.SeasonNumber) + .FirstOrDefault(); + + if (currentSeason == null) + return OnError("moonanime", proxyManager); + + MoonAnimeSeasonContent seasonData = firstSeasonData; + if (seasonData == null || !string.Equals(seasonData.Url, currentSeason.Url, StringComparison.OrdinalIgnoreCase)) + seasonData = await invoke.GetSeasonContent(currentSeason); + + if (seasonData == null || seasonData.Voices.Count == 0) + return OnError("moonanime", proxyManager); + + string displayTitle = !string.IsNullOrWhiteSpace(title) + ? title + : !string.IsNullOrWhiteSpace(originalTitle) + ? originalTitle + : "MoonAnime"; + + var movieTpl = new MovieTpl(displayTitle, originalTitle); + int fallbackIndex = 1; + + foreach (var voice in seasonData.Voices) + { + if (voice == null) + continue; + + string file = !string.IsNullOrWhiteSpace(voice.MovieFile) + ? voice.MovieFile + : voice.Episodes?.FirstOrDefault(e => !string.IsNullOrWhiteSpace(e.File))?.File; + + if (string.IsNullOrWhiteSpace(file)) + continue; + + string voiceName = string.IsNullOrWhiteSpace(voice.Name) ? $"Озвучка {fallbackIndex}" : voice.Name; + string callUrl = $"{host}/moonanime/play?file={HttpUtility.UrlEncode(file)}&title={HttpUtility.UrlEncode(displayTitle)}"; + movieTpl.Append(voiceName, accsArgs(callUrl), "call"); + fallbackIndex++; + } + + if (movieTpl.IsEmpty) + return OnError("moonanime", proxyManager); + + return rjson + ? Content(movieTpl.ToJson(), "application/json; charset=utf-8") + : Content(movieTpl.ToHtml(), "text/html; charset=utf-8"); + } + + private string BuildIndexUrl(string imdbId, long kinopoiskId, string title, string originalTitle, int year, int serial, string malId, int season, string voice) + { + var url = new StringBuilder(); + url.Append($"{host}/moonanime?imdb_id={HttpUtility.UrlEncode(imdbId)}"); + url.Append($"&kinopoisk_id={kinopoiskId}"); + url.Append($"&title={HttpUtility.UrlEncode(title)}"); + url.Append($"&original_title={HttpUtility.UrlEncode(originalTitle)}"); + url.Append($"&year={year}"); + url.Append($"&serial={serial}"); + + if (!string.IsNullOrWhiteSpace(malId)) + url.Append($"&mal_id={HttpUtility.UrlEncode(malId)}"); + + if (season > 0) + url.Append($"&s={season}"); + + if (!string.IsNullOrWhiteSpace(voice)) + url.Append($"&t={HttpUtility.UrlEncode(voice)}"); + + return url.ToString(); + } + + private int ParseVoiceIndex(string voiceValue, int totalVoices) + { + if (totalVoices <= 0) + return 0; + + if (!int.TryParse(voiceValue, out int index)) + return 0; + + if (index < 0 || index >= totalVoices) + return 0; + + return index; + } + + private string BuildStreamUrl(OnlinesSettings init, string streamLink) + { + string link = StripLampacArgs(streamLink?.Trim()); + if (string.IsNullOrEmpty(link)) + return link; + + var headers = new List + { + new HeadersModel("User-Agent", "Mozilla/5.0"), + new HeadersModel("Referer", "https://moonanime.art/") + }; + + if (ApnHelper.IsEnabled(init)) + { + if (ModInit.ApnHostProvided) + return ApnHelper.WrapUrl(init, link); + + var noApn = (OnlinesSettings)init.Clone(); + noApn.apnstream = false; + noApn.apn = null; + return HostStreamProxy(noApn, link, headers: headers, proxy: proxyManager.Get()); + } + + return HostStreamProxy(init, link, headers: headers, proxy: proxyManager.Get()); + } + + private static string StripLampacArgs(string url) + { + if (string.IsNullOrEmpty(url)) + return url; + + string cleaned = Regex.Replace( + url, + @"([?&])(account_email|uid|nws_id)=[^&]*", + "$1", + RegexOptions.IgnoreCase + ); + + cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&'); + return cleaned; + } + } +} diff --git a/MoonAnime/ModInit.cs b/MoonAnime/ModInit.cs new file mode 100644 index 0000000..6be49a5 --- /dev/null +++ b/MoonAnime/ModInit.cs @@ -0,0 +1,194 @@ +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Shared; +using Shared.Engine; +using Shared.Models.Module; +using Shared.Models.Online.Settings; +using System; +using System.Net.Http; +using System.Net.Mime; +using System.Net.Security; +using System.Security.Authentication; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MoonAnime +{ + public class ModInit + { + public static double Version => 1.0; + + public static OnlinesSettings MoonAnime; + + public static bool ApnHostProvided; + + public static OnlinesSettings Settings + { + get => MoonAnime; + set => MoonAnime = value; + } + + /// + /// Модуль завантажено. + /// + public static void loaded(InitspaceModel initspace) + { + MoonAnime = new OnlinesSettings("MoonAnime", "https://moonanime.art", "https://apx.lme.isroot.in", streamproxy: false, useproxy: false) + { + displayname = "MoonAnime", + displayindex = 0, + proxy = new Shared.Models.Base.ProxySettings() + { + useAuth = true, + username = "", + password = "", + list = new string[] { "socks5://ip:port" } + } + }; + + var conf = ModuleInvoke.Conf("MoonAnime", MoonAnime) ?? JObject.FromObject(MoonAnime); + bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost); + conf.Remove("apn"); + conf.Remove("apn_host"); + MoonAnime = conf.ToObject(); + + if (hasApn) + ApnHelper.ApplyInitConf(apnEnabled, apnHost, MoonAnime); + + ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost); + if (hasApn && apnEnabled) + { + MoonAnime.streamproxy = false; + } + else if (MoonAnime.streamproxy) + { + MoonAnime.apnstream = false; + MoonAnime.apn = null; + } + + AppInit.conf.online.with_search.Add("moonanime"); + } + } + + public static class UpdateService + { + private static readonly string _connectUrl = "https://lmcuk.lampame.v6.rocks/stats"; + + private static ConnectResponse? Connect = null; + private static DateTime? _connectTime = null; + private static DateTime? _disconnectTime = null; + + private static readonly TimeSpan _resetInterval = TimeSpan.FromHours(4); + private static Timer? _resetTimer = null; + + private static readonly object _lock = new(); + + public static async Task ConnectAsync(string host, CancellationToken cancellationToken = default) + { + if (_connectTime is not null || Connect?.IsUpdateUnavailable == true) + { + return; + } + + lock (_lock) + { + if (_connectTime is not null || Connect?.IsUpdateUnavailable == true) + { + return; + } + + _connectTime = DateTime.UtcNow; + } + + try + { + using var handler = new SocketsHttpHandler + { + SslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = (_, _, _, _) => true, + EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13 + } + }; + + using var client = new HttpClient(handler); + client.Timeout = TimeSpan.FromSeconds(15); + + var request = new + { + Host = host, + Module = ModInit.Settings.plugin, + Version = ModInit.Version, + }; + + var requestJson = JsonConvert.SerializeObject(request, Formatting.None); + var requestContent = new StringContent(requestJson, Encoding.UTF8, MediaTypeNames.Application.Json); + + var response = await client + .PostAsync(_connectUrl, requestContent, cancellationToken) + .ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + if (response.Content.Headers.ContentLength > 0) + { + var responseText = await response.Content + .ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + + Connect = JsonConvert.DeserializeObject(responseText); + } + + lock (_lock) + { + _resetTimer?.Dispose(); + _resetTimer = null; + + if (Connect?.IsUpdateUnavailable != true) + { + _resetTimer = new Timer(ResetConnectTime, null, _resetInterval, Timeout.InfiniteTimeSpan); + } + else + { + _disconnectTime = Connect?.IsNoiseEnabled == true + ? DateTime.UtcNow.AddHours(Random.Shared.Next(1, 4)) + : DateTime.UtcNow; + } + } + } + catch (Exception) + { + ResetConnectTime(null); + } + } + + private static void ResetConnectTime(object? state) + { + lock (_lock) + { + _connectTime = null; + Connect = null; + + _resetTimer?.Dispose(); + _resetTimer = null; + } + } + + public static bool IsDisconnected() + { + return _disconnectTime is not null + && DateTime.UtcNow >= _disconnectTime; + } + + public static ActionResult Validate(ActionResult result) + { + return IsDisconnected() + ? throw new JsonReaderException($"Disconnect error: {Guid.CreateVersion7()}") + : result; + } + } + + public record ConnectResponse(bool IsUpdateUnavailable, bool IsNoiseEnabled); +} diff --git a/MoonAnime/Models/MoonAnimeModels.cs b/MoonAnime/Models/MoonAnimeModels.cs new file mode 100644 index 0000000..8bf9726 --- /dev/null +++ b/MoonAnime/Models/MoonAnimeModels.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace MoonAnime.Models +{ + public class MoonAnimeSearchResponse + { + [JsonPropertyName("seasons")] + public List Seasons { get; set; } = new(); + } + + public class MoonAnimeSeasonRef + { + [JsonPropertyName("season_number")] + public int SeasonNumber { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + } + + public class MoonAnimeSeasonContent + { + public int SeasonNumber { get; set; } + + public string Url { get; set; } + + public bool IsSeries { get; set; } + + public List Voices { get; set; } = new(); + } + + public class MoonAnimeVoiceContent + { + public string Name { get; set; } + + public string MovieFile { get; set; } + + public List Episodes { get; set; } = new(); + } + + public class MoonAnimeEpisodeContent + { + public string Name { get; set; } + + public int Number { get; set; } + + public string File { get; set; } + } + + public class MoonAnimeStreamVariant + { + public string Url { get; set; } + + public string Quality { get; set; } + } +} diff --git a/MoonAnime/MoonAnime.csproj b/MoonAnime/MoonAnime.csproj new file mode 100644 index 0000000..1fbe365 --- /dev/null +++ b/MoonAnime/MoonAnime.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + library + true + + + + + ..\..\Shared.dll + + + + diff --git a/MoonAnime/MoonAnimeInvoke.cs b/MoonAnime/MoonAnimeInvoke.cs new file mode 100644 index 0000000..df6a742 --- /dev/null +++ b/MoonAnime/MoonAnimeInvoke.cs @@ -0,0 +1,462 @@ +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); + } + } +} diff --git a/MoonAnime/OnlineApi.cs b/MoonAnime/OnlineApi.cs new file mode 100644 index 0000000..6666152 --- /dev/null +++ b/MoonAnime/OnlineApi.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Shared.Models; +using Shared.Models.Base; +using Shared.Models.Module; +using System.Collections.Generic; + +namespace MoonAnime +{ + public class OnlineApi + { + public static List<(string name, string url, string plugin, int index)> Invoke( + HttpContext httpContext, + IMemoryCache memoryCache, + RequestModel requestInfo, + string host, + OnlineEventsModel args) + { + long.TryParse(args.id, out long tmdbid); + return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email); + } + + public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email) + { + var online = new List<(string name, string url, string plugin, int index)>(); + + var init = ModInit.MoonAnime; + + bool hasLang = !string.IsNullOrEmpty(original_language); + bool isAnime = hasLang && (original_language == "ja" || original_language == "zh"); + + if (init.enable && !init.rip && (serial == -1 || isAnime || !hasLang)) + { + string url = init.overridehost; + if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) + url = $"{host}/moonanime"; + + online.Add((init.displayname, url, "moonanime", init.displayindex)); + } + + return online; + } + } +} diff --git a/MoonAnime/manifest.json b/MoonAnime/manifest.json new file mode 100644 index 0000000..754dcaa --- /dev/null +++ b/MoonAnime/manifest.json @@ -0,0 +1,6 @@ +{ + "enable": true, + "version": 3, + "initspace": "MoonAnime.ModInit", + "online": "MoonAnime.OnlineApi" +} diff --git a/README.md b/README.md index d5fb4e9..0b94ae5 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ - [x] BambooUA - [x] Unimay - [x] Mikai +- [x] MoonAnime ## Installation @@ -41,6 +42,7 @@ Create or update the module/repository.yaml file - AnimeON - Unimay - Mikai + - MoonAnime - Uaflix - Bamboo - Makhno @@ -161,6 +163,7 @@ Sources with APN support: - Mikai - Makhno - KlonFUN +- MoonAnime ## Source/player availability check script From dbb24205d7c1efc7bc1cf361ebb227ecbfca0066 Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 7 Mar 2026 17:12:49 +0200 Subject: [PATCH 2/5] refactor(nmoonanime): rename MoonAnime module to NMoonAnime --- AnimeON/ModInit.cs | 2 +- MoonAnime/manifest.json | 6 -- {MoonAnime => NMoonAnime}/ApnHelper.cs | 0 {MoonAnime => NMoonAnime}/Controller.cs | 72 +++++++++---------- {MoonAnime => NMoonAnime}/ModInit.cs | 28 ++++---- .../Models/NMoonAnimeModels.cs | 20 +++--- .../NMoonAnime.csproj | 0 .../NMoonAnimeInvoke.cs | 56 +++++++-------- {MoonAnime => NMoonAnime}/OnlineApi.cs | 8 +-- NMoonAnime/manifest.json | 6 ++ README.md | 6 +- 11 files changed, 102 insertions(+), 102 deletions(-) delete mode 100644 MoonAnime/manifest.json rename {MoonAnime => NMoonAnime}/ApnHelper.cs (100%) rename {MoonAnime => NMoonAnime}/Controller.cs (83%) rename {MoonAnime => NMoonAnime}/ModInit.cs (87%) rename MoonAnime/Models/MoonAnimeModels.cs => NMoonAnime/Models/NMoonAnimeModels.cs (61%) rename MoonAnime/MoonAnime.csproj => NMoonAnime/NMoonAnime.csproj (100%) rename MoonAnime/MoonAnimeInvoke.cs => NMoonAnime/NMoonAnimeInvoke.cs (87%) rename {MoonAnime => NMoonAnime}/OnlineApi.cs (88%) create mode 100644 NMoonAnime/manifest.json diff --git a/AnimeON/ModInit.cs b/AnimeON/ModInit.cs index 3f70166..b33d74e 100644 --- a/AnimeON/ModInit.cs +++ b/AnimeON/ModInit.cs @@ -45,7 +45,7 @@ namespace AnimeON AnimeON = new OnlinesSettings("AnimeON", "https://animeon.club", streamproxy: false, useproxy: false) { - displayname = "🇯🇵 AnimeON", + displayname = "AnimeON", displayindex = 0, proxy = new Shared.Models.Base.ProxySettings() { diff --git a/MoonAnime/manifest.json b/MoonAnime/manifest.json deleted file mode 100644 index 754dcaa..0000000 --- a/MoonAnime/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "enable": true, - "version": 3, - "initspace": "MoonAnime.ModInit", - "online": "MoonAnime.OnlineApi" -} diff --git a/MoonAnime/ApnHelper.cs b/NMoonAnime/ApnHelper.cs similarity index 100% rename from MoonAnime/ApnHelper.cs rename to NMoonAnime/ApnHelper.cs diff --git a/MoonAnime/Controller.cs b/NMoonAnime/Controller.cs similarity index 83% rename from MoonAnime/Controller.cs rename to NMoonAnime/Controller.cs index dd13a44..a934991 100644 --- a/MoonAnime/Controller.cs +++ b/NMoonAnime/Controller.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using MoonAnime.Models; +using NMoonAnime.Models; using Shared; using Shared.Engine; using Shared.Models; @@ -13,7 +13,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; -namespace MoonAnime.Controllers +namespace NMoonAnime.Controllers { public class Controller : BaseOnlineController { @@ -21,47 +21,47 @@ namespace MoonAnime.Controllers public Controller() : base(ModInit.Settings) { - proxyManager = new ProxyManager(ModInit.MoonAnime); + proxyManager = new ProxyManager(ModInit.NMoonAnime); } [HttpGet] - [Route("moonanime")] + [Route("nmoonanime")] public async Task Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string mal_id, string t, int s = -1, bool rjson = false, bool checksearch = false) { await UpdateService.ConnectAsync(host); - var init = await loadKit(ModInit.MoonAnime); + var init = await loadKit(ModInit.NMoonAnime); if (!init.enable) return Forbid(); - var invoke = new MoonAnimeInvoke(init, hybridCache, OnLog, proxyManager); + var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager); if (checksearch) { if (AppInit.conf?.online?.checkOnlineSearch != true) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); var checkResults = await invoke.Search(imdb_id, mal_id, title, original_title, year); if (checkResults != null && checkResults.Count > 0) return Content("data-json=", "text/plain; charset=utf-8"); - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); } - OnLog($"MoonAnime: title={title}, original_title={original_title}, imdb={imdb_id}, mal_id={mal_id}, serial={serial}, s={s}, t={t}"); + OnLog($"NMoonAnime: назва={title}, оригінальна_назва={original_title}, imdb={imdb_id}, mal_id={mal_id}, серіал={serial}, сезон={s}, озвучка={t}"); var seasons = await invoke.Search(imdb_id, mal_id, title, original_title, year); if (seasons == null || seasons.Count == 0) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); bool isSeries = serial == 1; - MoonAnimeSeasonContent firstSeasonData = null; + NMoonAnimeSeasonContent firstSeasonData = null; if (serial == -1) { firstSeasonData = await invoke.GetSeasonContent(seasons[0]); if (firstSeasonData == null || firstSeasonData.Voices.Count == 0) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); isSeries = firstSeasonData.IsSeries; } @@ -74,22 +74,22 @@ namespace MoonAnime.Controllers return await RenderMovie(invoke, seasons, title, original_title, firstSeasonData, rjson); } - [HttpGet("moonanime/play")] + [HttpGet("nmoonanime/play")] public async Task Play(string file, string title = null) { await UpdateService.ConnectAsync(host); - var init = await loadKit(ModInit.MoonAnime); + var init = await loadKit(ModInit.NMoonAnime); if (!init.enable) return Forbid(); if (string.IsNullOrWhiteSpace(file)) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); - var invoke = new MoonAnimeInvoke(init, hybridCache, OnLog, proxyManager); + var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager); var streams = invoke.ParseStreams(file); if (streams == null || streams.Count == 0) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); if (streams.Count == 1) { @@ -106,7 +106,7 @@ namespace MoonAnime.Controllers } if (!streamQuality.Any()) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); var first = streamQuality.Firts(); string json = VideoTpl.ToJson("play", first.link, title ?? string.Empty, streamquality: streamQuality); @@ -114,8 +114,8 @@ namespace MoonAnime.Controllers } private async Task RenderSerial( - MoonAnimeInvoke invoke, - List seasons, + NMoonAnimeInvoke invoke, + List seasons, string imdbId, long kinopoiskId, string title, @@ -132,7 +132,7 @@ namespace MoonAnime.Controllers .ToList(); if (orderedSeasons.Count == 0) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); if (selectedSeason == -1) { @@ -153,14 +153,14 @@ namespace MoonAnime.Controllers var currentSeason = orderedSeasons.FirstOrDefault(s => s.SeasonNumber == selectedSeason) ?? orderedSeasons[0]; var seasonData = await invoke.GetSeasonContent(currentSeason); if (seasonData == null) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); var voices = seasonData.Voices .Where(v => v != null && v.Episodes != null && v.Episodes.Count > 0) .ToList(); if (voices.Count == 0) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); int activeVoiceIndex = ParseVoiceIndex(selectedVoice, voices.Count); var voiceTpl = new VoiceTpl(voices.Count); @@ -179,20 +179,20 @@ namespace MoonAnime.Controllers .ToList(); if (episodes.Count == 0) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); string displayTitle = !string.IsNullOrWhiteSpace(title) ? title : !string.IsNullOrWhiteSpace(originalTitle) ? originalTitle - : "MoonAnime"; + : "NMoonAnime"; var episodeTpl = new EpisodeTpl(episodes.Count); foreach (var episode in episodes) { int episodeNumber = episode.Number <= 0 ? 1 : episode.Number; string episodeName = string.IsNullOrWhiteSpace(episode.Name) ? $"Епізод {episodeNumber}" : episode.Name; - string callUrl = $"{host}/moonanime/play?file={HttpUtility.UrlEncode(episode.File)}&title={HttpUtility.UrlEncode(displayTitle)}"; + string callUrl = $"{host}/nmoonanime/play?file={HttpUtility.UrlEncode(episode.File)}&title={HttpUtility.UrlEncode(displayTitle)}"; episodeTpl.Append(episodeName, displayTitle, currentSeason.SeasonNumber.ToString(), episodeNumber.ToString(), accsArgs(callUrl), "call"); } @@ -204,11 +204,11 @@ namespace MoonAnime.Controllers } private async Task RenderMovie( - MoonAnimeInvoke invoke, - List seasons, + NMoonAnimeInvoke invoke, + List seasons, string title, string originalTitle, - MoonAnimeSeasonContent firstSeasonData, + NMoonAnimeSeasonContent firstSeasonData, bool rjson) { var currentSeason = seasons @@ -217,20 +217,20 @@ namespace MoonAnime.Controllers .FirstOrDefault(); if (currentSeason == null) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); - MoonAnimeSeasonContent seasonData = firstSeasonData; + NMoonAnimeSeasonContent seasonData = firstSeasonData; if (seasonData == null || !string.Equals(seasonData.Url, currentSeason.Url, StringComparison.OrdinalIgnoreCase)) seasonData = await invoke.GetSeasonContent(currentSeason); if (seasonData == null || seasonData.Voices.Count == 0) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); string displayTitle = !string.IsNullOrWhiteSpace(title) ? title : !string.IsNullOrWhiteSpace(originalTitle) ? originalTitle - : "MoonAnime"; + : "NMoonAnime"; var movieTpl = new MovieTpl(displayTitle, originalTitle); int fallbackIndex = 1; @@ -248,13 +248,13 @@ namespace MoonAnime.Controllers continue; string voiceName = string.IsNullOrWhiteSpace(voice.Name) ? $"Озвучка {fallbackIndex}" : voice.Name; - string callUrl = $"{host}/moonanime/play?file={HttpUtility.UrlEncode(file)}&title={HttpUtility.UrlEncode(displayTitle)}"; + string callUrl = $"{host}/nmoonanime/play?file={HttpUtility.UrlEncode(file)}&title={HttpUtility.UrlEncode(displayTitle)}"; movieTpl.Append(voiceName, accsArgs(callUrl), "call"); fallbackIndex++; } if (movieTpl.IsEmpty) - return OnError("moonanime", proxyManager); + return OnError("nmoonanime", proxyManager); return rjson ? Content(movieTpl.ToJson(), "application/json; charset=utf-8") @@ -264,7 +264,7 @@ namespace MoonAnime.Controllers private string BuildIndexUrl(string imdbId, long kinopoiskId, string title, string originalTitle, int year, int serial, string malId, int season, string voice) { var url = new StringBuilder(); - url.Append($"{host}/moonanime?imdb_id={HttpUtility.UrlEncode(imdbId)}"); + url.Append($"{host}/nmoonanime?imdb_id={HttpUtility.UrlEncode(imdbId)}"); url.Append($"&kinopoisk_id={kinopoiskId}"); url.Append($"&title={HttpUtility.UrlEncode(title)}"); url.Append($"&original_title={HttpUtility.UrlEncode(originalTitle)}"); diff --git a/MoonAnime/ModInit.cs b/NMoonAnime/ModInit.cs similarity index 87% rename from MoonAnime/ModInit.cs rename to NMoonAnime/ModInit.cs index 6be49a5..e60040b 100644 --- a/MoonAnime/ModInit.cs +++ b/NMoonAnime/ModInit.cs @@ -14,20 +14,20 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace MoonAnime +namespace NMoonAnime { public class ModInit { public static double Version => 1.0; - public static OnlinesSettings MoonAnime; + public static OnlinesSettings NMoonAnime; public static bool ApnHostProvided; public static OnlinesSettings Settings { - get => MoonAnime; - set => MoonAnime = value; + get => NMoonAnime; + set => NMoonAnime = value; } /// @@ -35,9 +35,9 @@ namespace MoonAnime /// public static void loaded(InitspaceModel initspace) { - MoonAnime = new OnlinesSettings("MoonAnime", "https://moonanime.art", "https://apx.lme.isroot.in", streamproxy: false, useproxy: false) + NMoonAnime = new OnlinesSettings("NMoonAnime", "https://moonanime.art", "https://apx.lme.isroot.in", streamproxy: false, useproxy: false) { - displayname = "MoonAnime", + displayname = "🌙 NMoonAnime", displayindex = 0, proxy = new Shared.Models.Base.ProxySettings() { @@ -48,27 +48,27 @@ namespace MoonAnime } }; - var conf = ModuleInvoke.Conf("MoonAnime", MoonAnime) ?? JObject.FromObject(MoonAnime); + var conf = ModuleInvoke.Conf("NMoonAnime", NMoonAnime) ?? JObject.FromObject(NMoonAnime); bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost); conf.Remove("apn"); conf.Remove("apn_host"); - MoonAnime = conf.ToObject(); + NMoonAnime = conf.ToObject(); if (hasApn) - ApnHelper.ApplyInitConf(apnEnabled, apnHost, MoonAnime); + ApnHelper.ApplyInitConf(apnEnabled, apnHost, NMoonAnime); ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost); if (hasApn && apnEnabled) { - MoonAnime.streamproxy = false; + NMoonAnime.streamproxy = false; } - else if (MoonAnime.streamproxy) + else if (NMoonAnime.streamproxy) { - MoonAnime.apnstream = false; - MoonAnime.apn = null; + NMoonAnime.apnstream = false; + NMoonAnime.apn = null; } - AppInit.conf.online.with_search.Add("moonanime"); + AppInit.conf.online.with_search.Add("nmoonanime"); } } diff --git a/MoonAnime/Models/MoonAnimeModels.cs b/NMoonAnime/Models/NMoonAnimeModels.cs similarity index 61% rename from MoonAnime/Models/MoonAnimeModels.cs rename to NMoonAnime/Models/NMoonAnimeModels.cs index 8bf9726..470d277 100644 --- a/MoonAnime/Models/MoonAnimeModels.cs +++ b/NMoonAnime/Models/NMoonAnimeModels.cs @@ -1,15 +1,15 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace MoonAnime.Models +namespace NMoonAnime.Models { - public class MoonAnimeSearchResponse + public class NMoonAnimeSearchResponse { [JsonPropertyName("seasons")] - public List Seasons { get; set; } = new(); + public List Seasons { get; set; } = new(); } - public class MoonAnimeSeasonRef + public class NMoonAnimeSeasonRef { [JsonPropertyName("season_number")] public int SeasonNumber { get; set; } @@ -18,7 +18,7 @@ namespace MoonAnime.Models public string Url { get; set; } } - public class MoonAnimeSeasonContent + public class NMoonAnimeSeasonContent { public int SeasonNumber { get; set; } @@ -26,19 +26,19 @@ namespace MoonAnime.Models public bool IsSeries { get; set; } - public List Voices { get; set; } = new(); + public List Voices { get; set; } = new(); } - public class MoonAnimeVoiceContent + public class NMoonAnimeVoiceContent { public string Name { get; set; } public string MovieFile { get; set; } - public List Episodes { get; set; } = new(); + public List Episodes { get; set; } = new(); } - public class MoonAnimeEpisodeContent + public class NMoonAnimeEpisodeContent { public string Name { get; set; } @@ -47,7 +47,7 @@ namespace MoonAnime.Models public string File { get; set; } } - public class MoonAnimeStreamVariant + public class NMoonAnimeStreamVariant { public string Url { get; set; } diff --git a/MoonAnime/MoonAnime.csproj b/NMoonAnime/NMoonAnime.csproj similarity index 100% rename from MoonAnime/MoonAnime.csproj rename to NMoonAnime/NMoonAnime.csproj diff --git a/MoonAnime/MoonAnimeInvoke.cs b/NMoonAnime/NMoonAnimeInvoke.cs similarity index 87% rename from MoonAnime/MoonAnimeInvoke.cs rename to NMoonAnime/NMoonAnimeInvoke.cs index df6a742..d2e087b 100644 --- a/MoonAnime/MoonAnimeInvoke.cs +++ b/NMoonAnime/NMoonAnimeInvoke.cs @@ -1,4 +1,4 @@ -using MoonAnime.Models; +using NMoonAnime.Models; using Shared; using Shared.Engine; using Shared.Models; @@ -12,9 +12,9 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; -namespace MoonAnime +namespace NMoonAnime { - public class MoonAnimeInvoke + public class NMoonAnimeInvoke { private readonly OnlinesSettings _init; private readonly IHybridCache _hybridCache; @@ -25,7 +25,7 @@ namespace MoonAnime PropertyNameCaseInsensitive = true }; - public MoonAnimeInvoke(OnlinesSettings init, IHybridCache hybridCache, Action onLog, ProxyManager proxyManager) + public NMoonAnimeInvoke(OnlinesSettings init, IHybridCache hybridCache, Action onLog, ProxyManager proxyManager) { _init = init; _hybridCache = hybridCache; @@ -33,10 +33,10 @@ namespace MoonAnime _proxyManager = proxyManager; } - public async Task> Search(string imdbId, string malId, string title, string originalTitle, int year) + 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)) + string memKey = $"NMoonAnime:search:{imdbId}:{malId}:{title}:{originalTitle}:{year}"; + if (_hybridCache.TryGetValue(memKey, out List cached)) return cached; try @@ -53,15 +53,15 @@ namespace MoonAnime if (string.IsNullOrWhiteSpace(searchUrl)) continue; - _onLog($"MoonAnime: пошук через {searchUrl}"); + _onLog($"NMoonAnime: пошук через {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 response = JsonSerializer.Deserialize(json, _jsonOptions); var seasons = response?.Seasons? .Where(s => s != null && !string.IsNullOrWhiteSpace(s.Url)) - .Select(s => new MoonAnimeSeasonRef + .Select(s => new NMoonAnimeSeasonRef { SeasonNumber = s.SeasonNumber <= 0 ? 1 : s.SeasonNumber, Url = s.Url.Trim() @@ -80,24 +80,24 @@ namespace MoonAnime } catch (Exception ex) { - _onLog($"MoonAnime: помилка пошуку - {ex.Message}"); + _onLog($"NMoonAnime: помилка пошуку - {ex.Message}"); } - return new List(); + return new List(); } - public async Task GetSeasonContent(MoonAnimeSeasonRef season) + public async Task GetSeasonContent(NMoonAnimeSeasonRef season) { if (season == null || string.IsNullOrWhiteSpace(season.Url)) return null; - string memKey = $"MoonAnime:season:{season.Url}"; - if (_hybridCache.TryGetValue(memKey, out MoonAnimeSeasonContent cached)) + string memKey = $"NMoonAnime:season:{season.Url}"; + if (_hybridCache.TryGetValue(memKey, out NMoonAnimeSeasonContent cached)) return cached; try { - _onLog($"MoonAnime: завантаження сезону {season.Url}"); + _onLog($"NMoonAnime: завантаження сезону {season.Url}"); string html = await Http.Get(_init.cors(season.Url), headers: DefaultHeaders(), proxy: _proxyManager.Get()); if (string.IsNullOrWhiteSpace(html)) return null; @@ -110,14 +110,14 @@ namespace MoonAnime } catch (Exception ex) { - _onLog($"MoonAnime: помилка читання сезону - {ex.Message}"); + _onLog($"NMoonAnime: помилка читання сезону - {ex.Message}"); return null; } } - public List ParseStreams(string rawFile) + public List ParseStreams(string rawFile) { - var streams = new List(); + var streams = new List(); if (string.IsNullOrWhiteSpace(rawFile)) return streams; @@ -131,7 +131,7 @@ namespace MoonAnime if (string.IsNullOrWhiteSpace(url)) continue; - streams.Add(new MoonAnimeStreamVariant + streams.Add(new NMoonAnimeStreamVariant { Url = url, Quality = quality @@ -148,7 +148,7 @@ namespace MoonAnime if (string.IsNullOrWhiteSpace(url)) continue; - streams.Add(new MoonAnimeStreamVariant + streams.Add(new NMoonAnimeStreamVariant { Url = url, Quality = quality @@ -167,7 +167,7 @@ namespace MoonAnime { for (int i = 0; i < plainLinks.Count; i++) { - streams.Add(new MoonAnimeStreamVariant + streams.Add(new NMoonAnimeStreamVariant { Url = plainLinks[i], Quality = $"auto-{i + 1}" @@ -178,7 +178,7 @@ namespace MoonAnime if (streams.Count == 0 && value.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - streams.Add(new MoonAnimeStreamVariant + streams.Add(new NMoonAnimeStreamVariant { Url = value, Quality = "auto" @@ -187,7 +187,7 @@ namespace MoonAnime return streams .Where(s => s != null && !string.IsNullOrWhiteSpace(s.Url)) - .Select(s => new MoonAnimeStreamVariant + .Select(s => new NMoonAnimeStreamVariant { Url = s.Url.Trim(), Quality = NormalizeQuality(s.Quality) @@ -223,9 +223,9 @@ namespace MoonAnime return $"{_init.apihost.TrimEnd('/')}{endpoint}?{query}"; } - private MoonAnimeSeasonContent ParseSeasonPage(string html, int seasonNumber, string seasonUrl) + private NMoonAnimeSeasonContent ParseSeasonPage(string html, int seasonNumber, string seasonUrl) { - var content = new MoonAnimeSeasonContent + var content = new NMoonAnimeSeasonContent { SeasonNumber = seasonNumber <= 0 ? 1 : seasonNumber, Url = seasonUrl, @@ -246,7 +246,7 @@ namespace MoonAnime if (entry.ValueKind != JsonValueKind.Object) continue; - var voice = new MoonAnimeVoiceContent + var voice = new NMoonAnimeVoiceContent { Name = NormalizeVoiceName(GetStringProperty(entry, "title"), voiceIndex) }; @@ -266,7 +266,7 @@ namespace MoonAnime string episodeTitle = GetStringProperty(episodeEntry, "title"); int episodeNumber = ParseEpisodeNumber(episodeTitle, episodeIndex); - voice.Episodes.Add(new MoonAnimeEpisodeContent + voice.Episodes.Add(new NMoonAnimeEpisodeContent { Name = string.IsNullOrWhiteSpace(episodeTitle) ? $"Епізод {episodeNumber}" : WebUtility.HtmlDecode(episodeTitle), Number = episodeNumber, diff --git a/MoonAnime/OnlineApi.cs b/NMoonAnime/OnlineApi.cs similarity index 88% rename from MoonAnime/OnlineApi.cs rename to NMoonAnime/OnlineApi.cs index 6666152..d432193 100644 --- a/MoonAnime/OnlineApi.cs +++ b/NMoonAnime/OnlineApi.cs @@ -5,7 +5,7 @@ using Shared.Models.Base; using Shared.Models.Module; using System.Collections.Generic; -namespace MoonAnime +namespace NMoonAnime { public class OnlineApi { @@ -24,7 +24,7 @@ namespace MoonAnime { var online = new List<(string name, string url, string plugin, int index)>(); - var init = ModInit.MoonAnime; + var init = ModInit.NMoonAnime; bool hasLang = !string.IsNullOrEmpty(original_language); bool isAnime = hasLang && (original_language == "ja" || original_language == "zh"); @@ -33,9 +33,9 @@ namespace MoonAnime { string url = init.overridehost; if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) - url = $"{host}/moonanime"; + url = $"{host}/nmoonanime"; - online.Add((init.displayname, url, "moonanime", init.displayindex)); + online.Add((init.displayname, url, "nmoonanime", init.displayindex)); } return online; diff --git a/NMoonAnime/manifest.json b/NMoonAnime/manifest.json new file mode 100644 index 0000000..8f47d66 --- /dev/null +++ b/NMoonAnime/manifest.json @@ -0,0 +1,6 @@ +{ + "enable": true, + "version": 3, + "initspace": "NMoonAnime.ModInit", + "online": "NMoonAnime.OnlineApi" +} diff --git a/README.md b/README.md index 0b94ae5..211fb4d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - [x] BambooUA - [x] Unimay - [x] Mikai -- [x] MoonAnime +- [x] NMoonAnime ## Installation @@ -42,7 +42,7 @@ Create or update the module/repository.yaml file - AnimeON - Unimay - Mikai - - MoonAnime + - NMoonAnime - Uaflix - Bamboo - Makhno @@ -163,7 +163,7 @@ Sources with APN support: - Mikai - Makhno - KlonFUN -- MoonAnime +- NMoonAnime ## Source/player availability check script From 4532c621c717eeeb2758b823f575d21972ef5cf8 Mon Sep 17 00:00:00 2001 From: Felix Date: Sun, 8 Mar 2026 08:28:05 +0200 Subject: [PATCH 3/5] refactor(nmoonanime): simplify search method by removing original_title parameter - Add ResolveMalId helper to resolve effective MAL ID from mal_id or kinopoisk_id - Remove original_title from Search method signature and cache keys - Simplify BuildSearchUrl to prioritize query parameters (mal_id > imdb_id > title) --- NMoonAnime/Controller.cs | 17 +++++++++++++---- NMoonAnime/NMoonAnimeInvoke.cs | 21 +++++++++------------ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/NMoonAnime/Controller.cs b/NMoonAnime/Controller.cs index a934991..df1afee 100644 --- a/NMoonAnime/Controller.cs +++ b/NMoonAnime/Controller.cs @@ -35,22 +35,23 @@ namespace NMoonAnime.Controllers return Forbid(); var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager); + string effectiveMalId = ResolveMalId(mal_id, kinopoisk_id); if (checksearch) { if (AppInit.conf?.online?.checkOnlineSearch != true) return OnError("nmoonanime", proxyManager); - var checkResults = await invoke.Search(imdb_id, mal_id, title, original_title, year); + var checkResults = await invoke.Search(imdb_id, effectiveMalId, title, year); if (checkResults != null && checkResults.Count > 0) return Content("data-json=", "text/plain; charset=utf-8"); return OnError("nmoonanime", proxyManager); } - OnLog($"NMoonAnime: назва={title}, оригінальна_назва={original_title}, imdb={imdb_id}, mal_id={mal_id}, серіал={serial}, сезон={s}, озвучка={t}"); + OnLog($"NMoonAnime: назва={title}, imdb={imdb_id}, kinopoisk_id(як mal_id)={kinopoisk_id}, mal_id_ефективний={effectiveMalId}, рік={year}, серіал={serial}, сезон={s}, озвучка={t}"); - var seasons = await invoke.Search(imdb_id, mal_id, title, original_title, year); + var seasons = await invoke.Search(imdb_id, effectiveMalId, title, year); if (seasons == null || seasons.Count == 0) return OnError("nmoonanime", proxyManager); @@ -68,7 +69,7 @@ namespace NMoonAnime.Controllers if (isSeries) { - return await RenderSerial(invoke, seasons, imdb_id, kinopoisk_id, title, original_title, year, mal_id, s, t, rjson); + return await RenderSerial(invoke, seasons, imdb_id, kinopoisk_id, title, original_title, year, effectiveMalId, s, t, rjson); } return await RenderMovie(invoke, seasons, title, original_title, firstSeasonData, rjson); @@ -297,6 +298,14 @@ namespace NMoonAnime.Controllers return index; } + private static string ResolveMalId(string malId, long kinopoiskId) + { + if (!string.IsNullOrWhiteSpace(malId)) + return malId.Trim(); + + return kinopoiskId > 0 ? kinopoiskId.ToString() : null; + } + private string BuildStreamUrl(OnlinesSettings init, string streamLink) { string link = StripLampacArgs(streamLink?.Trim()); diff --git a/NMoonAnime/NMoonAnimeInvoke.cs b/NMoonAnime/NMoonAnimeInvoke.cs index d2e087b..ce7ce3f 100644 --- a/NMoonAnime/NMoonAnimeInvoke.cs +++ b/NMoonAnime/NMoonAnimeInvoke.cs @@ -33,9 +33,9 @@ namespace NMoonAnime _proxyManager = proxyManager; } - public async Task> Search(string imdbId, string malId, string title, string originalTitle, int year) + public async Task> Search(string imdbId, string malId, string title, int year) { - string memKey = $"NMoonAnime:search:{imdbId}:{malId}:{title}:{originalTitle}:{year}"; + string memKey = $"NMoonAnime:search:{imdbId}:{malId}:{title}:{year}"; if (_hybridCache.TryGetValue(memKey, out List cached)) return cached; @@ -49,7 +49,7 @@ namespace NMoonAnime foreach (var endpoint in endpoints) { - string searchUrl = BuildSearchUrl(endpoint, imdbId, malId, title, originalTitle, year); + string searchUrl = BuildSearchUrl(endpoint, imdbId, malId, title, year); if (string.IsNullOrWhiteSpace(searchUrl)) continue; @@ -198,21 +198,18 @@ namespace NMoonAnime .ToList(); } - private string BuildSearchUrl(string endpoint, string imdbId, string malId, string title, string originalTitle, int year) + private string BuildSearchUrl(string endpoint, string imdbId, string malId, string title, 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)) + else if (!string.IsNullOrWhiteSpace(imdbId)) + query["imdb_id"] = imdbId; + else if (!string.IsNullOrWhiteSpace(title)) query["title"] = title; - - if (!string.IsNullOrWhiteSpace(originalTitle)) - query["original_title"] = originalTitle; + else + return null; if (year > 0) query["year"] = year.ToString(); From cecc70abe4923281373742075fcf3e424c453c64 Mon Sep 17 00:00:00 2001 From: Felix Date: Sun, 8 Mar 2026 08:37:01 +0200 Subject: [PATCH 4/5] refactor(nmoonanime): update display name --- NMoonAnime/ModInit.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NMoonAnime/ModInit.cs b/NMoonAnime/ModInit.cs index e60040b..c08e90a 100644 --- a/NMoonAnime/ModInit.cs +++ b/NMoonAnime/ModInit.cs @@ -37,7 +37,7 @@ namespace NMoonAnime { NMoonAnime = new OnlinesSettings("NMoonAnime", "https://moonanime.art", "https://apx.lme.isroot.in", streamproxy: false, useproxy: false) { - displayname = "🌙 NMoonAnime", + displayname = "New MoonAnime", displayindex = 0, proxy = new Shared.Models.Base.ProxySettings() { From 2a91e2076e45c8fdfc352c8ae96f8686a118f142 Mon Sep 17 00:00:00 2001 From: Felix Date: Sun, 8 Mar 2026 08:45:44 +0200 Subject: [PATCH 5/5] refactor(nmoonanime): modify ResolveMalId to handle source parameter Update ResolveMalId method to accept and process a source parameter, enabling different resolution logic for tmdb and hikka sources. TMDB source now returns null, while HIKKA source uses kinopoiskId when available. --- NMoonAnime/Controller.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/NMoonAnime/Controller.cs b/NMoonAnime/Controller.cs index df1afee..cdf81f0 100644 --- a/NMoonAnime/Controller.cs +++ b/NMoonAnime/Controller.cs @@ -35,7 +35,7 @@ namespace NMoonAnime.Controllers return Forbid(); var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager); - string effectiveMalId = ResolveMalId(mal_id, kinopoisk_id); + string effectiveMalId = ResolveMalId(mal_id, kinopoisk_id, source); if (checksearch) { @@ -49,7 +49,7 @@ namespace NMoonAnime.Controllers return OnError("nmoonanime", proxyManager); } - OnLog($"NMoonAnime: назва={title}, imdb={imdb_id}, kinopoisk_id(як mal_id)={kinopoisk_id}, mal_id_ефективний={effectiveMalId}, рік={year}, серіал={serial}, сезон={s}, озвучка={t}"); + OnLog($"NMoonAnime: назва={title}, source={source}, imdb={imdb_id}, kinopoisk_id(як mal_id)={kinopoisk_id}, mal_id_ефективний={effectiveMalId}, рік={year}, серіал={serial}, сезон={s}, озвучка={t}"); var seasons = await invoke.Search(imdb_id, effectiveMalId, title, year); if (seasons == null || seasons.Count == 0) @@ -298,12 +298,18 @@ namespace NMoonAnime.Controllers return index; } - private static string ResolveMalId(string malId, long kinopoiskId) + private static string ResolveMalId(string malId, long kinopoiskId, string source) { + if (!string.IsNullOrWhiteSpace(source) && source.Equals("tmdb", StringComparison.OrdinalIgnoreCase)) + return null; + if (!string.IsNullOrWhiteSpace(malId)) return malId.Trim(); - return kinopoiskId > 0 ? kinopoiskId.ToString() : null; + if (!string.IsNullOrWhiteSpace(source) && source.Equals("hikka", StringComparison.OrdinalIgnoreCase) && kinopoiskId > 0) + return kinopoiskId.ToString(); + + return null; } private string BuildStreamUrl(OnlinesSettings init, string streamLink)