Compare commits

..

10 Commits

Author SHA1 Message Date
Felix
b8f3ea7568 docs: rename project title to Lampac NextGen 2026-04-09 15:50:54 +03:00
Felix
ff90f149f0 chore: update .gitignore to exclude vscode settings
Add .vscode/settings.json to the ignore list to prevent committing local editor configurations.
2026-04-04 12:17:53 +03:00
Felix
b1a7ce510d feat(uafilmme): add UafilmME streaming plugin with APN support
Integrate a new online streaming source for UafilmME, including API invocation, search, and playback functionality. Adds APN proxy helper for Ashdi streams, module initialization, and related models and controllers to extend the existing online framework.
2026-04-04 12:09:18 +03:00
Felix
0aed459fab perf(uaflix): implement lazy season parsing for serials
Refactor season selection logic to use lazy loading instead of full aggregation, improving performance when choosing seasons. Added GetSeasonIndex and GetSeasonEpisodes methods, and SeasonUrls property to PaginationInfo for efficient season URL management.
2026-04-04 08:48:37 +03:00
Felix
31549455ee feat(apn): add magic_apn support for Ashdi streams
Introduce a new configuration option `magic_apn` that allows automatic enabling of APN for Ashdi streams when using the inner player. The configuration is an object with an `ashdi` property that specifies the host to use for Ashdi APN.

Changes include:
- Add `TryGetMagicAshdiHost` method to parse `magic_apn` configuration
- Add `NormalizeHost`
2026-04-01 18:54:32 +03:00
Felix
0cb8412036
Merge pull request #24 from levende/lampac-ng
Add magic apn
2026-04-01 18:22:00 +03:00
Oleksandr Zhyzhchenko
fc7ddf2668 Add magic apn 2026-04-01 15:02:33 +03:00
Felix
ee5fae6581
Merge pull request #23 from levende/lampac-ng
Migration NG
2026-03-31 15:23:10 +03:00
Oleksandr Zhyzhchenko
312be86e27 Migration NG 2026-03-31 14:24:00 +03:00
Felix
6aece92fd0 refactor!: migrate all modules to new module interface architecture
BREAKING CHANGE: All module routes changed from /{module} to /lite/{module}

- Implement IModuleLoaded and IModuleOnline interfaces across all modules
- Add HttpHydra support to all Invoke classes for HTTP request handling
- Replace ModuleInvoke.Conf() with ModuleInvoke.Init() in all ModInit classes
- Convert loadKit() from async to synchronous calls in all controllers
- Replace direct AppInit.conf.online.with_search.Add() with reflection-based
  RegisterWithSearch() method for decoupled module registration
- Simplify cacheTime() logic by removing mikrotik/multiaccess conditionals
- Add GlobalUsings.cs to all modules for shared namespace imports
- Update OnlineApi to use ModuleOnlineItem instead of value tuples
- Bump all module versions to new major versions
2026-03-28 10:18:21 +02:00
89 changed files with 3734 additions and 726 deletions

6
.gitignore vendored
View File

@ -10,4 +10,8 @@
/.qodo/
.DS_Store
AGENTS.md
/planing/
/planing/
.vs
bin
obj
.vscode/settings.json

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>

View File

@ -23,13 +23,15 @@ namespace AnimeON
private IHybridCache _hybridCache;
private Action<string> _onLog;
private ProxyManager _proxyManager;
private readonly HttpHydra _httpHydra;
public AnimeONInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
public AnimeONInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_httpHydra = httpHydra;
}
string AshdiRequestUrl(string url)
@ -61,7 +63,7 @@ namespace AnimeON
string searchUrl = $"{_init.host}/api/anime/search?text={System.Web.HttpUtility.UrlEncode(query)}";
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {searchUrl}");
string searchJson = await Http.Get(_init.cors(searchUrl), headers: headers, proxy: _proxyManager.Get());
string searchJson = await HttpGet(searchUrl, headers);
if (string.IsNullOrEmpty(searchJson))
return null;
@ -124,7 +126,7 @@ namespace AnimeON
string fundubsUrl = $"{_init.host}/api/player/{animeId}/translations";
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {fundubsUrl}");
string fundubsJson = await Http.Get(_init.cors(fundubsUrl), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get());
string fundubsJson = await HttpGet(fundubsUrl, new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) });
if (string.IsNullOrEmpty(fundubsJson))
return null;
@ -150,7 +152,7 @@ namespace AnimeON
string episodesUrl = $"{_init.host}/api/player/{animeId}/episodes?take=100&skip=-1&playerId={playerId}&translationId={fundubId}";
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {episodesUrl}");
string episodesJson = await Http.Get(_init.cors(episodesUrl), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get());
string episodesJson = await HttpGet(episodesUrl, new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) });
if (string.IsNullOrEmpty(episodesJson))
return null;
@ -169,7 +171,7 @@ namespace AnimeON
};
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
string html = await HttpGet(requestUrl, headers);
if (string.IsNullOrEmpty(html))
return null;
@ -206,7 +208,7 @@ namespace AnimeON
string requestUrl = AshdiRequestUrl(WithAshdiMultivoice(url, enable: !disableAshdiMultivoiceForVod));
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
string html = await HttpGet(requestUrl, headers);
if (string.IsNullOrEmpty(html))
return streams;
@ -263,7 +265,7 @@ namespace AnimeON
string url = $"{_init.host}/api/player/{episodeId}/episode";
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {url}");
string json = await Http.Get(_init.cors(url), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get());
string json = await HttpGet(url, new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) });
if (string.IsNullOrEmpty(json))
return null;
@ -474,12 +476,21 @@ namespace AnimeON
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;
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;
return TimeSpan.FromMinutes(ctime);
}
private Task<string> HttpGet(string url, List<HeadersModel> headers)
{
if (_httpHydra != null)
return _httpHydra.Get(url, newheaders: headers);
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
}
public async Task<AnimeON.Models.AnimeONAggregatedStructure> AggregateSerialStructure(int animeId, int season)
{
string memKey = $"AnimeON:aggregated:{animeId}:{season}";

View File

@ -25,6 +25,23 @@ namespace Shared.Engine
return true;
}
public static string TryGetMagicAshdiHost(JObject conf)
{
if (conf == null || !conf.TryGetValue("magic_apn", out var magicToken) || magicToken == null)
return null;
if (magicToken.Type == JTokenType.Boolean)
return magicToken.Value<bool>() ? DefaultHost : null;
if (magicToken.Type == JTokenType.String)
return NormalizeHost(magicToken.Value<string>());
if (magicToken.Type != JTokenType.Object)
return null;
return NormalizeHost(((JObject)magicToken).Value<string>("ashdi"));
}
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
{
if (init == null)
@ -37,8 +54,13 @@ namespace Shared.Engine
return;
}
if (string.IsNullOrWhiteSpace(host))
host = DefaultHost;
host = NormalizeHost(host);
if (host == null)
{
init.apnstream = false;
init.apn = null;
return;
}
if (init.apn == null)
init.apn = new ApnConf();
@ -82,5 +104,13 @@ namespace Shared.Engine
return $"{host.TrimEnd('/')}/{url}";
}
private static string NormalizeHost(string host)
{
if (string.IsNullOrWhiteSpace(host))
return null;
return host.Trim();
}
}
}

View File

@ -27,27 +27,28 @@ namespace AnimeON.Controllers
}
[HttpGet]
[Route("animeon")]
[Route("lite/animeon")]
async public Task<ActionResult> 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 t, int s = -1, bool rjson = false, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.AnimeON);
var init = loadKit(ModInit.AnimeON);
if (!init.enable)
return Forbid();
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager);
TryEnableMagicApn(init);
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch)
{
if (AppInit.conf?.online?.checkOnlineSearch != true)
return OnError("animeon", proxyManager);
if (!IsCheckOnlineSearchEnabled())
return OnError("animeon", refresh_proxy: true);
var checkSeasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial);
if (checkSeasons != null && checkSeasons.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8");
return OnError("animeon", proxyManager);
return OnError("animeon", refresh_proxy: true);
}
OnLog($"AnimeON Index: title={title}, original_title={original_title}, serial={serial}, s={s}, t={t}, year={year}, imdb_id={imdb_id}, kp={kinopoisk_id}");
@ -55,7 +56,7 @@ namespace AnimeON.Controllers
var seasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial);
OnLog($"AnimeON: search results = {seasons?.Count ?? 0}");
if (seasons == null || seasons.Count == 0)
return OnError("animeon", proxyManager);
return OnError("animeon", refresh_proxy: true);
// [Refactoring] Використовується агрегована структура (AggregateSerialStructure) — попередній збір allOptions не потрібний
@ -81,7 +82,7 @@ namespace AnimeON.Controllers
foreach (var item in seasonItems)
{
string seasonName = item.SeasonNumber.ToString();
string link = $"{host}/animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={item.SeasonNumber}";
string link = $"{host}/lite/animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={item.SeasonNumber}";
season_tpl.Append(seasonName, link, seasonName);
}
OnLog($"AnimeON: return seasons count={seasonItems.Count}");
@ -106,13 +107,13 @@ namespace AnimeON.Controllers
selected = new { Anime = seasons[s], Index = s, SeasonNumber = seasons[s].Season > 0 ? seasons[s].Season : s + 1 };
if (selected == null)
return OnError("animeon", proxyManager);
return OnError("animeon", refresh_proxy: true);
var selectedAnime = selected.Anime;
int selectedSeasonNumber = selected.SeasonNumber;
var structure = await invoke.AggregateSerialStructure(selectedAnime.Id, selectedSeasonNumber);
if (structure == null || !structure.Voices.Any())
return OnError("animeon", proxyManager);
return OnError("animeon", refresh_proxy: true);
OnLog($"AnimeON: voices found = {structure.Voices.Count}");
var voiceItems = structure.Voices
@ -135,14 +136,14 @@ namespace AnimeON.Controllers
var voice_tpl = new VoiceTpl();
foreach (var voice in voiceItems)
{
string voiceLink = $"{host}/animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
string voiceLink = $"{host}/lite/animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
bool isActive = voice.Key == t;
voice_tpl.Append(voice.Display, isActive, voiceLink);
}
// Перевірка вибраної озвучки
if (!structure.Voices.ContainsKey(t))
return OnError("animeon", proxyManager);
return OnError("animeon", refresh_proxy: true);
var episode_tpl = new EpisodeTpl();
var selectedVoiceInfo = structure.Voices[t];
@ -179,7 +180,7 @@ namespace AnimeON.Controllers
if (string.IsNullOrEmpty(streamLink) && ep.EpisodeId > 0)
{
string callUrl = $"{host}/animeon/play?episode_id={ep.EpisodeId}&serial=1";
string callUrl = $"{host}/lite/animeon/play?episode_id={ep.EpisodeId}&serial=1";
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
continue;
}
@ -189,7 +190,7 @@ namespace AnimeON.Controllers
if (needsResolve || streamLink.Contains("moonanime.art") || streamLink.Contains("ashdi.vip/vod"))
{
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}&serial=1";
string callUrl = $"{host}/lite/animeon/play?url={HttpUtility.UrlEncode(streamLink)}&serial=1";
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
}
else
@ -212,12 +213,12 @@ namespace AnimeON.Controllers
{
var firstAnime = seasons.FirstOrDefault();
if (firstAnime == null)
return OnError("animeon", proxyManager);
return OnError("animeon", refresh_proxy: true);
var fundubs = await invoke.GetFundubs(firstAnime.Id);
OnLog($"AnimeON: movie fundubs count = {fundubs?.Count ?? 0}");
if (fundubs == null || fundubs.Count == 0)
return OnError("animeon", proxyManager);
return OnError("animeon", refresh_proxy: true);
var tpl = new MovieTpl(title, original_title);
@ -251,7 +252,7 @@ namespace AnimeON.Controllers
foreach (var ashdiStream in ashdiStreams)
{
string optionName = $"{translationName} {ashdiStream.title}";
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(ashdiStream.link)}";
string callUrl = $"{host}/lite/animeon/play?url={HttpUtility.UrlEncode(ashdiStream.link)}";
tpl.Append(optionName, accsArgs(callUrl), "call");
}
continue;
@ -260,7 +261,7 @@ namespace AnimeON.Controllers
if (needsResolve || streamLink.Contains("moonanime.art/iframe/") || streamLink.Contains("ashdi.vip/vod"))
{
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";
string callUrl = $"{host}/lite/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";
tpl.Append(translationName, accsArgs(callUrl), "call");
}
else
@ -272,7 +273,7 @@ namespace AnimeON.Controllers
// Якщо не зібрали жодної опції — повертаємо помилку
if (tpl.data == null || tpl.data.Count == 0)
return OnError("animeon", proxyManager);
return OnError("animeon", refresh_proxy: true);
OnLog("AnimeON: return movie options");
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8");
@ -283,7 +284,7 @@ namespace AnimeON.Controllers
{
string fundubsUrl = $"{init.host}/api/player/{animeId}/translations";
string fundubsJson = await Http.Get(init.cors(fundubsUrl), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
string fundubsJson = await httpHydra.Get(fundubsUrl, newheaders: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
if (string.IsNullOrEmpty(fundubsJson))
return null;
@ -308,7 +309,7 @@ namespace AnimeON.Controllers
{
string episodesUrl = $"{init.host}/api/player/{animeId}/episodes?take=100&skip=-1&playerId={playerId}&translationId={fundubId}";
string episodesJson = await Http.Get(init.cors(episodesUrl), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
string episodesJson = await httpHydra.Get(episodesUrl, newheaders: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
if (string.IsNullOrEmpty(episodesJson))
return null;
@ -332,7 +333,7 @@ namespace AnimeON.Controllers
string searchUrl = $"{init.host}/api/anime/search?text={HttpUtility.UrlEncode(query)}";
string searchJson = await Http.Get(init.cors(searchUrl), headers: headers);
string searchJson = await httpHydra.Get(searchUrl, newheaders: headers);
if (string.IsNullOrEmpty(searchJson))
return null;
@ -373,16 +374,17 @@ namespace AnimeON.Controllers
return null;
}
[HttpGet("animeon/play")]
[HttpGet("lite/animeon/play")]
public async Task<ActionResult> Play(string url, int episode_id = 0, string title = null, int serial = 0)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.AnimeON);
var init = loadKit(ModInit.AnimeON);
if (!init.enable)
return Forbid();
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager);
TryEnableMagicApn(init);
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
bool disableAshdiMultivoiceForVod = serial == 1;
OnLog($"AnimeON Play: url={url}, episode_id={episode_id}, serial={serial}");
@ -398,13 +400,13 @@ namespace AnimeON.Controllers
else
{
OnLog("AnimeON Play: empty url");
return OnError("animeon", proxyManager);
return OnError("animeon", refresh_proxy: true);
}
if (string.IsNullOrEmpty(streamLink))
{
OnLog("AnimeON Play: cannot extract stream");
return OnError("animeon", proxyManager);
return OnError("animeon", refresh_proxy: true);
}
List<HeadersModel> streamHeaders = null;
@ -462,5 +464,56 @@ namespace AnimeON.Controllers
return HostStreamProxy(init, link, headers: headers, force_streamproxy: forceProxy);
}
private void TryEnableMagicApn(OnlinesSettings init)
{
if (init == null
|| init.apn != null
|| init.streamproxy
|| string.IsNullOrWhiteSpace(ModInit.MagicApnAshdiHost))
return;
string player = new RchClient(HttpContext, host, init, requestInfo).InfoConnected()?.player;
bool useInnerPlayer = string.IsNullOrWhiteSpace(player)
|| player.Equals("inner", StringComparison.OrdinalIgnoreCase);
if (!useInnerPlayer)
return;
ApnHelper.ApplyInitConf(true, ModInit.MagicApnAshdiHost, init);
OnLog($"AnimeON: увімкнено magic_apn для Ashdi (player={player ?? "unknown"}).");
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

4
AnimeON/GlobalUsings.cs Normal file
View File

@ -0,0 +1,4 @@
global using Shared.Services;
global using Shared.Services.Hybrid;
global using Shared.Models.Base;
global using AppInit = Shared.CoreInit;

View File

@ -3,6 +3,7 @@ using Newtonsoft.Json.Linq;
using Shared;
using Shared.Engine;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Shared.Models.Online.Settings;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting;
@ -23,12 +24,13 @@ using System.Threading.Tasks;
namespace AnimeON
{
public class ModInit
public class ModInit : IModuleLoaded
{
public static double Version => 3.7;
public static double Version => 4.0;
public static OnlinesSettings AnimeON;
public static bool ApnHostProvided;
public static string MagicApnAshdiHost;
public static OnlinesSettings Settings
{
@ -39,7 +41,7 @@ namespace AnimeON
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
public void Loaded(InitspaceModel initspace)
{
@ -55,15 +57,23 @@ namespace AnimeON
list = new string[] { "socks5://ip:port" }
}
};
var conf = ModuleInvoke.Conf("AnimeON", AnimeON);
var defaults = JObject.FromObject(AnimeON);
defaults["magic_apn"] = new JObject()
{
["ashdi"] = ApnHelper.DefaultHost
};
var conf = ModuleInvoke.Init("AnimeON", defaults) ?? defaults;
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
MagicApnAshdiHost = ApnHelper.TryGetMagicAshdiHost(conf);
conf.Remove("magic_apn");
conf.Remove("apn");
conf.Remove("apn_host");
AnimeON = conf.ToObject<OnlinesSettings>();
if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, AnimeON);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
if (hasApn && apnEnabled)
ApnHostProvided = ApnHelper.IsEnabled(AnimeON);
if (ApnHostProvided)
{
AnimeON.streamproxy = false;
}
@ -74,7 +84,45 @@ namespace AnimeON
}
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("animeon");
RegisterWithSearch("animeon");
}
private static void RegisterWithSearch(string plugin)
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
{
foreach (var item in list)
{
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
return;
}
list.Add(plugin);
}
}
catch
{
}
}
public void Dispose()
{
}
}
@ -196,4 +244,4 @@ namespace AnimeON
}
public record ConnectResponse(bool IsUpdateUnavailable, bool IsNoiseEnabled);
}
}

View File

@ -1,47 +1,36 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module;
using System.Collections.Generic;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AnimeON
{
public class OnlineApi
public class OnlineApi : IModuleOnline
{
public static List<(string name, string url, string plugin, int index)> Invoke(
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, 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)
private static List<ModuleOnlineItem> 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 online = new List<ModuleOnlineItem>();
var init = ModInit.AnimeON;
// Визначаємо isAnime згідно стандарту Lampac (Deepwiki):
// isanime = true якщо original_language == "ja" або "zh"
bool hasLang = !string.IsNullOrEmpty(original_language);
bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
// AnimeON — аніме-провайдер. Додаємо його:
// - при загальному пошуку (serial == -1), або
// - якщо контент визначений як аніме (isanime), або
// - якщо мова невідома (відсутній original_language)
if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang))
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
url = $"{host}/animeon";
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add((init.displayname, url, "animeon", init.displayindex));
online.Add(new ModuleOnlineItem(init, "animeon"));
}
return online;

View File

@ -1,6 +1,3 @@
{
"enable": true,
"version": 3,
"initspace": "AnimeON.ModInit",
"online": "AnimeON.OnlineApi"
"enable": true
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>

View File

@ -20,13 +20,15 @@ namespace Bamboo
private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
private readonly HttpHydra _httpHydra;
public BambooInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
public BambooInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_httpHydra = httpHydra;
}
public async Task<List<SearchResult>> Search(string title, string original_title)
@ -50,7 +52,7 @@ namespace Bamboo
};
_onLog?.Invoke($"Bamboo search: {searchUrl}");
string html = await Http.Get(_init.cors(searchUrl), headers: headers, proxy: _proxyManager.Get());
string html = await HttpGet(searchUrl, headers);
if (string.IsNullOrEmpty(html))
return null;
@ -109,7 +111,7 @@ namespace Bamboo
};
_onLog?.Invoke($"Bamboo series page: {href}");
string html = await Http.Get(_init.cors(href), headers: headers, proxy: _proxyManager.Get());
string html = await HttpGet(href, headers);
if (string.IsNullOrEmpty(html))
return null;
@ -183,7 +185,7 @@ namespace Bamboo
};
_onLog?.Invoke($"Bamboo movie page: {href}");
string html = await Http.Get(_init.cors(href), headers: headers, proxy: _proxyManager.Get());
string html = await HttpGet(href, headers);
if (string.IsNullOrEmpty(html))
return null;
@ -311,12 +313,20 @@ namespace Bamboo
return HtmlEntity.DeEntitize(value).Trim();
}
private Task<string> HttpGet(string url, List<HeadersModel> headers)
{
if (_httpHydra != null)
return _httpHydra.Get(url, newheaders: headers);
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
}
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;
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;

View File

@ -23,27 +23,27 @@ namespace Bamboo.Controllers
}
[HttpGet]
[Route("bamboo")]
[Route("lite/bamboo")]
async public Task<ActionResult> 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 t, int s = -1, bool rjson = false, string href = null, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Bamboo);
var init = loadKit(ModInit.Bamboo);
if (!init.enable)
return Forbid();
var invoke = new BambooInvoke(init, hybridCache, OnLog, proxyManager);
var invoke = new BambooInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch)
{
if (AppInit.conf?.online?.checkOnlineSearch != true)
return OnError("bamboo", proxyManager);
if (!IsCheckOnlineSearchEnabled())
return OnError("bamboo", refresh_proxy: true);
var searchResults = await invoke.Search(title, original_title);
if (searchResults != null && searchResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8");
return OnError("bamboo", proxyManager);
return OnError("bamboo", refresh_proxy: true);
}
string itemUrl = href;
@ -51,14 +51,14 @@ namespace Bamboo.Controllers
{
var searchResults = await invoke.Search(title, original_title);
if (searchResults == null || searchResults.Count == 0)
return OnError("bamboo", proxyManager);
return OnError("bamboo", refresh_proxy: true);
if (searchResults.Count > 1)
{
var similar_tpl = new SimilarTpl(searchResults.Count);
foreach (var res in searchResults)
{
string link = $"{host}/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}";
string link = $"{host}/lite/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}";
similar_tpl.Append(res.Title, string.Empty, string.Empty, link, res.Poster);
}
@ -72,7 +72,7 @@ namespace Bamboo.Controllers
{
var series = await invoke.GetSeriesEpisodes(itemUrl);
if (series == null || (series.Sub.Count == 0 && series.Dub.Count == 0))
return OnError("bamboo", proxyManager);
return OnError("bamboo", refresh_proxy: true);
var voice_tpl = new VoiceTpl();
var episode_tpl = new EpisodeTpl();
@ -88,13 +88,13 @@ namespace Bamboo.Controllers
foreach (var voice in availableVoices)
{
string voiceLink = $"{host}/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&t={voice.key}&href={HttpUtility.UrlEncode(itemUrl)}";
string voiceLink = $"{host}/lite/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&t={voice.key}&href={HttpUtility.UrlEncode(itemUrl)}";
voice_tpl.Append(voice.name, voice.key == t, voiceLink);
}
var selected = availableVoices.FirstOrDefault(v => v.key == t);
if (selected.episodes == null || selected.episodes.Count == 0)
return OnError("bamboo", proxyManager);
return OnError("bamboo", refresh_proxy: true);
int index = 1;
foreach (var ep in selected.episodes.OrderBy(e => e.Episode ?? int.MaxValue))
@ -116,7 +116,7 @@ namespace Bamboo.Controllers
{
var streams = await invoke.GetMovieStreams(itemUrl);
if (streams == null || streams.Count == 0)
return OnError("bamboo", proxyManager);
return OnError("bamboo", refresh_proxy: true);
var movie_tpl = new MovieTpl(title, original_title);
for (int i = 0; i < streams.Count; i++)
@ -166,5 +166,38 @@ namespace Bamboo.Controllers
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned;
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

4
Bamboo/GlobalUsings.cs Normal file
View File

@ -0,0 +1,4 @@
global using Shared.Services;
global using Shared.Services.Hybrid;
global using Shared.Models.Base;
global using AppInit = Shared.CoreInit;

View File

@ -3,6 +3,7 @@ using Shared;
using Shared.Engine;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Caching.Memory;
@ -22,9 +23,9 @@ using System.Threading.Tasks;
namespace Bamboo
{
public class ModInit
public class ModInit : IModuleLoaded
{
public static double Version => 3.6;
public static double Version => 4.0;
public static OnlinesSettings Bamboo;
public static bool ApnHostProvided;
@ -38,7 +39,7 @@ namespace Bamboo
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
public void Loaded(InitspaceModel initspace)
{
@ -54,7 +55,7 @@ namespace Bamboo
list = new string[] { "socks5://ip:port" }
}
};
var conf = ModuleInvoke.Conf("Bamboo", Bamboo);
var conf = ModuleInvoke.Init("Bamboo", JObject.FromObject(Bamboo));
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
conf.Remove("apn");
conf.Remove("apn_host");
@ -73,7 +74,45 @@ namespace Bamboo
}
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("bamboo");
RegisterWithSearch("bamboo");
}
private static void RegisterWithSearch(string plugin)
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
{
foreach (var item in list)
{
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
return;
}
list.Add(plugin);
}
}
catch
{
}
}
public void Dispose()
{
}
}

View File

@ -1,28 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Bamboo
{
public class OnlineApi
public class OnlineApi : IModuleOnline
{
public static List<(string name, string url, string plugin, int index)> Invoke(
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, 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)
private static List<ModuleOnlineItem> 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 online = new List<ModuleOnlineItem>();
var init = ModInit.Bamboo;
if (init.enable && !init.rip)
@ -34,11 +30,10 @@ namespace Bamboo
return online;
}
string url = init.overridehost;
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
url = $"{host}/bamboo";
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add((init.displayname, url, "bamboo", init.displayindex));
online.Add(new ModuleOnlineItem(init, "bamboo"));
}
return online;

View File

@ -1,6 +1,3 @@
{
"enable": true,
"version": 3,
"initspace": "Bamboo.ModInit",
"online": "Bamboo.OnlineApi"
"enable": true
}

View File

@ -1,10 +1,9 @@
using JackTor.Models;
using Microsoft.AspNetCore.Mvc;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.PiTor;
using Shared.Models.Online.Settings;
using Shared.Services.Utilities;
using Shared.Models.Templates;
using System;
using System.Collections.Generic;
@ -27,7 +26,7 @@ namespace JackTor.Controllers
}
[HttpGet]
[Route("jacktor")]
[Route("lite/jacktor")]
async public Task<ActionResult> Index(
long id,
string imdb_id,
@ -46,7 +45,7 @@ namespace JackTor.Controllers
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Settings);
var init = loadKit(ModInit.Settings);
if (!init.enable)
return Forbid();
@ -57,14 +56,14 @@ namespace JackTor.Controllers
if (checksearch)
{
if (AppInit.conf?.online?.checkOnlineSearch != true)
return OnError("jacktor", proxyManager);
if (!IsCheckOnlineSearchEnabled())
return OnError("jacktor", refresh_proxy: true);
var check = await invoke.Search(title, original_title, year, serial, original_language);
if (check.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8");
return OnError("jacktor", proxyManager);
return OnError("jacktor", refresh_proxy: true);
}
var torrents = await invoke.Search(title, original_title, year, serial, original_language);
@ -98,7 +97,7 @@ namespace JackTor.Controllers
{
seasonTpl.Append(
$"{season} сезон",
$"{host}/jacktor?rjson={rjson}&title={enTitle}&original_title={enOriginal}&year={year}&original_language={original_language}&serial=1&s={season}",
$"{host}/lite/jacktor?rjson={rjson}&title={enTitle}&original_title={enOriginal}&year={year}&original_language={original_language}&serial=1&s={season}",
season);
}
@ -127,7 +126,7 @@ namespace JackTor.Controllers
: $"{seasonLabel} • {torrent.Voice}";
string qualityInfo = $"{torrent.Tracker} / {torrent.QualityLabel} / {torrent.MediaInfo} / ↑{torrent.Seeders}";
string releaseLink = accsArgs($"{host}/jacktor/serial/{torrent.Rid}?rjson={rjson}&title={enTitle}&original_title={enOriginal}&s={targetSeason}");
string releaseLink = accsArgs($"{host}/lite/jacktor/serial/{torrent.Rid}?rjson={rjson}&title={enTitle}&original_title={enOriginal}&s={targetSeason}");
similarTpl.Append(releaseName, null, qualityInfo, releaseLink);
}
@ -147,7 +146,7 @@ namespace JackTor.Controllers
: torrent.Voice;
string voiceName = $"{torrent.QualityLabel} / {torrent.MediaInfo} / ↑{torrent.Seeders}";
string streamLink = accsArgs($"{host}/jacktor/s{torrent.Rid}");
string streamLink = accsArgs($"{host}/lite/jacktor/s{torrent.Rid}");
movieTpl.Append(
voice,
@ -163,10 +162,10 @@ namespace JackTor.Controllers
}
[HttpGet]
[Route("jacktor/serial/{rid}")]
[Route("lite/jacktor/serial/{rid}")]
async public ValueTask<ActionResult> Serial(string rid, string account_email, string title, string original_title, int s = 1, bool rjson = false)
{
var init = await loadKit(ModInit.Settings);
var init = loadKit(ModInit.Settings);
if (!init.enable)
return Forbid();
@ -175,7 +174,7 @@ namespace JackTor.Controllers
var invoke = new JackTorInvoke(init, hybridCache, OnLog, proxyManager);
if (!invoke.TryGetSource(rid, out JackTorSourceCache source))
return OnError("jacktor", proxyManager);
return OnError("jacktor", refresh_proxy: true);
string memKey = $"jacktor:serial:{rid}";
@ -185,49 +184,49 @@ namespace JackTor.Controllers
{
var ts = ResolveProbeTorrentServer(init, account_email);
if (string.IsNullOrWhiteSpace(ts.host))
return OnError("jacktor", proxyManager);
return OnError("jacktor", refresh_proxy: true);
string hashResponse = await Http.Post(
string hashResponse = await httpHydra.Post(
$"{ts.host}/torrents",
BuildAddPayload(source.SourceUri),
timeoutSeconds: 8,
headers: ts.headers);
statusCodeOK: false,
newheaders: ts.headers);
string hash = ExtractHash(hashResponse);
if (string.IsNullOrWhiteSpace(hash))
return OnError("jacktor", proxyManager);
return OnError("jacktor", refresh_proxy: true);
Stat stat = null;
DateTime deadline = DateTime.Now.AddSeconds(20);
while (true)
{
stat = await Http.Post<Stat>(
stat = await httpHydra.Post<Stat>(
$"{ts.host}/torrents",
BuildGetPayload(hash),
timeoutSeconds: 3,
headers: ts.headers);
statusCodeOK: false,
newheaders: ts.headers);
if (stat?.file_stats != null && stat.file_stats.Length > 0)
break;
if (DateTime.Now > deadline)
{
_ = Http.Post($"{ts.host}/torrents", BuildRemovePayload(hash), headers: ts.headers);
return OnError("jacktor", proxyManager);
_ = httpHydra.Post($"{ts.host}/torrents", BuildRemovePayload(hash), statusCodeOK: false, newheaders: ts.headers);
return OnError("jacktor", refresh_proxy: true);
}
await Task.Delay(250);
}
_ = Http.Post($"{ts.host}/torrents", BuildRemovePayload(hash), headers: ts.headers);
_ = httpHydra.Post($"{ts.host}/torrents", BuildRemovePayload(hash), statusCodeOK: false, newheaders: ts.headers);
fileStats = stat.file_stats;
hybridCache.Set(memKey, fileStats, DateTime.Now.AddHours(36));
}
if (fileStats == null || fileStats.Length == 0)
return OnError("jacktor", proxyManager);
return OnError("jacktor", refresh_proxy: true);
var episodeTpl = new EpisodeTpl();
int appended = 0;
@ -242,13 +241,13 @@ namespace JackTor.Controllers
title ?? original_title,
s.ToString(),
file.Id.ToString(),
accsArgs($"{host}/jacktor/s{rid}?tsid={file.Id}"));
accsArgs($"{host}/lite/jacktor/s{rid}?tsid={file.Id}"));
appended++;
}
if (appended == 0)
return OnError("jacktor", proxyManager);
return OnError("jacktor", refresh_proxy: true);
return rjson
? Content(episodeTpl.ToJson(), "application/json; charset=utf-8")
@ -257,10 +256,10 @@ namespace JackTor.Controllers
}
[HttpGet]
[Route("jacktor/s{rid}")]
[Route("lite/jacktor/s{rid}")]
async public ValueTask<ActionResult> Stream(string rid, int tsid = -1, string account_email = null)
{
var init = await loadKit(ModInit.Settings);
var init = loadKit(ModInit.Settings);
if (!init.enable)
return Forbid();
@ -269,7 +268,7 @@ namespace JackTor.Controllers
var invoke = new JackTorInvoke(init, hybridCache, OnLog, proxyManager);
if (!invoke.TryGetSource(rid, out JackTorSourceCache source))
return OnError("jacktor", proxyManager);
return OnError("jacktor", refresh_proxy: true);
int index = tsid != -1 ? tsid : 1;
string country = requestInfo.Country;
@ -284,15 +283,15 @@ namespace JackTor.Controllers
var headers = HeadersModel.Init("Authorization", $"Basic {CrypTo.Base64($"{login}:{passwd}")}");
headers = HeadersModel.Join(headers, addheaders);
string response = await Http.Post(
string response = await httpHydra.Post(
$"{tsHost}/torrents",
BuildAddPayload(source.SourceUri),
timeoutSeconds: 5,
headers: headers);
statusCodeOK: false,
newheaders: headers);
hash = ExtractHash(response);
if (string.IsNullOrWhiteSpace(hash))
return OnError("jacktor", proxyManager);
return OnError("jacktor", refresh_proxy: true);
hybridCache.Set(memKey, hash, DateTime.Now.AddMinutes(1));
}
@ -329,7 +328,7 @@ namespace JackTor.Controllers
}
if (servers.Count == 0)
return OnError("jacktor", proxyManager);
return OnError("jacktor", refresh_proxy: true);
ts = servers[Random.Shared.Next(0, servers.Count)];
hybridCache.Set(tsKey, ts, DateTime.Now.AddHours(4));
@ -342,7 +341,7 @@ namespace JackTor.Controllers
if (init.base_auth != null && init.base_auth.enable)
{
if (init.torrs == null || init.torrs.Length == 0)
return OnError("jacktor", proxyManager);
return OnError("jacktor", refresh_proxy: true);
string tsKey = $"jacktor:ts3:{rid}:{requestInfo.IP}";
if (!hybridCache.TryGetValue(tsKey, out string tsHost))
@ -355,7 +354,7 @@ namespace JackTor.Controllers
}
if (init.torrs == null || init.torrs.Length == 0)
return OnError("jacktor", proxyManager);
return OnError("jacktor", refresh_proxy: true);
string key = $"jacktor:ts4:{rid}:{requestInfo.IP}";
if (!hybridCache.TryGetValue(key, out string torrentHost))
@ -474,5 +473,38 @@ namespace JackTor.Controllers
return Regex.Replace(url, "(apikey=)[^&]+", "$1***", RegexOptions.IgnoreCase);
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

4
JackTor/GlobalUsings.cs Normal file
View File

@ -0,0 +1,4 @@
global using Shared.Services;
global using Shared.Services.Hybrid;
global using Shared.Models.Base;
global using AppInit = Shared.CoreInit;

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>

View File

@ -1,5 +1,4 @@
using JackTor.Models;
using Shared.Engine;
using Shared.Models;
using System;
using System.Collections.Generic;

View File

@ -2,8 +2,8 @@ using JackTor.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Shared;
using Shared.Engine;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System;
using System.Net.Http;
using System.Net.Mime;
@ -15,9 +15,9 @@ using System.Threading.Tasks;
namespace JackTor
{
public class ModInit
public class ModInit : IModuleLoaded
{
public static double Version => 1.0;
public static double Version => 2.0;
public static JackTorSettings JackTor;
@ -30,7 +30,7 @@ namespace JackTor
/// <summary>
/// Модуль завантажено.
/// </summary>
public static void loaded(InitspaceModel initspace)
public void Loaded(InitspaceModel initspace)
{
JackTor = new JackTorSettings("JackTor", "http://127.0.0.1:9117", streamproxy: false, useproxy: false)
{
@ -66,7 +66,7 @@ namespace JackTor
}
};
var conf = ModuleInvoke.Conf("JackTor", JackTor) ?? JObject.FromObject(JackTor);
var conf = ModuleInvoke.Init("JackTor", JObject.FromObject(JackTor)) ?? JObject.FromObject(JackTor);
JackTor = conf.ToObject<JackTorSettings>();
if (string.IsNullOrWhiteSpace(JackTor.jackett))
@ -76,7 +76,45 @@ namespace JackTor
JackTor.host = JackTor.jackett;
// Показувати «уточнити пошук».
AppInit.conf.online.with_search.Add("jacktor");
RegisterWithSearch("jacktor");
}
private static void RegisterWithSearch(string plugin)
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
{
foreach (var item in list)
{
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
return;
}
list.Add(plugin);
}
}
catch
{
}
}
public void Dispose()
{
}
}

View File

@ -197,4 +197,18 @@ namespace JackTor.Models
public int[] Seasons { get; set; }
}
public class FileStat
{
public int Id { get; set; }
public string Path { get; set; }
public long Length { get; set; }
}
public class Stat
{
public FileStat[] file_stats { get; set; }
}
}

View File

@ -2,46 +2,31 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Shared.Models;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace JackTor
{
public class OnlineApi
public class OnlineApi : IModuleOnline
{
public static List<(string name, string url, string plugin, int index)> Invoke(
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, 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)
private static List<ModuleOnlineItem> 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 online = new List<ModuleOnlineItem>();
var init = ModInit.JackTor;
if (init.enable && !init.rip)
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
url = $"{host}/jacktor";
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add((init.displayname, url, "jacktor", init.displayindex));
online.Add(new ModuleOnlineItem(init, "jacktor"));
}
return online;

View File

@ -1,6 +1,3 @@
{
"enable": true,
"version": 3,
"initspace": "JackTor.ModInit",
"online": "JackTor.OnlineApi"
"enable": true
}

View File

@ -25,6 +25,23 @@ namespace Shared.Engine
return true;
}
public static string TryGetMagicAshdiHost(JObject conf)
{
if (conf == null || !conf.TryGetValue("magic_apn", out var magicToken) || magicToken == null)
return null;
if (magicToken.Type == JTokenType.Boolean)
return magicToken.Value<bool>() ? DefaultHost : null;
if (magicToken.Type == JTokenType.String)
return NormalizeHost(magicToken.Value<string>());
if (magicToken.Type != JTokenType.Object)
return null;
return NormalizeHost(((JObject)magicToken).Value<string>("ashdi"));
}
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
{
if (init == null)
@ -37,8 +54,13 @@ namespace Shared.Engine
return;
}
if (string.IsNullOrWhiteSpace(host))
host = DefaultHost;
host = NormalizeHost(host);
if (host == null)
{
init.apnstream = false;
init.apn = null;
return;
}
if (init.apn == null)
init.apn = new ApnConf();
@ -82,5 +104,13 @@ namespace Shared.Engine
return $"{host.TrimEnd('/')}/{url}";
}
private static string NormalizeHost(string host)
{
if (string.IsNullOrWhiteSpace(host))
return null;
return host.Trim();
}
}
}

View File

@ -22,27 +22,29 @@ namespace KlonFUN.Controllers
}
[HttpGet]
[Route("klonfun")]
[Route("lite/klonfun")]
async public Task<ActionResult> 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 t, int s = -1, bool rjson = false, string href = null, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.KlonFUN);
var init = loadKit(ModInit.KlonFUN);
if (!init.enable)
return Forbid();
var invoke = new KlonFUNInvoke(init, hybridCache, OnLog, proxyManager);
TryEnableMagicApn(init);
var invoke = new KlonFUNInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch)
{
if (AppInit.conf?.online?.checkOnlineSearch != true)
return OnError("klonfun", proxyManager);
if (!IsCheckOnlineSearchEnabled())
return OnError("klonfun", refresh_proxy: true);
var checkResults = await invoke.Search(imdb_id, title, original_title);
if (checkResults != null && checkResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8");
return OnError("klonfun", proxyManager);
return OnError("klonfun", refresh_proxy: true);
}
string itemUrl = href;
@ -50,14 +52,14 @@ namespace KlonFUN.Controllers
{
var searchResults = await invoke.Search(imdb_id, title, original_title);
if (searchResults == null || searchResults.Count == 0)
return OnError("klonfun", proxyManager);
return OnError("klonfun", refresh_proxy: true);
if (searchResults.Count > 1)
{
var similarTpl = new SimilarTpl(searchResults.Count);
foreach (SearchResult result in searchResults)
{
string link = $"{host}/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(result.Url)}";
string link = $"{host}/lite/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(result.Url)}";
similarTpl.Append(result.Title, result.Year > 0 ? result.Year.ToString() : string.Empty, string.Empty, link, result.Poster);
}
@ -73,7 +75,7 @@ namespace KlonFUN.Controllers
if (item == null || string.IsNullOrWhiteSpace(item.PlayerUrl))
{
OnLog($"KlonFUN: не знайдено iframe-плеєр для {itemUrl}");
return OnError("klonfun", proxyManager);
return OnError("klonfun", refresh_proxy: true);
}
string contentTitle = !string.IsNullOrWhiteSpace(title) ? title : item.Title;
@ -87,7 +89,7 @@ namespace KlonFUN.Controllers
{
var serialStructure = await invoke.GetSerialStructure(item.PlayerUrl);
if (serialStructure == null || serialStructure.Voices.Count == 0)
return OnError("klonfun", proxyManager);
return OnError("klonfun", refresh_proxy: true);
if (s == -1)
{
@ -118,12 +120,12 @@ namespace KlonFUN.Controllers
}
if (seasons.Count == 0)
return OnError("klonfun", proxyManager);
return OnError("klonfun", refresh_proxy: true);
var seasonTpl = new SeasonTpl(seasons.Count);
foreach (int seasonNumber in seasons)
{
string link = $"{host}/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}&href={HttpUtility.UrlEncode(itemUrl)}";
string link = $"{host}/lite/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}&href={HttpUtility.UrlEncode(itemUrl)}";
if (!string.IsNullOrWhiteSpace(t))
link += $"&t={HttpUtility.UrlEncode(t)}";
@ -140,7 +142,7 @@ namespace KlonFUN.Controllers
.ToList();
if (voicesForSeason.Count == 0)
return OnError("klonfun", proxyManager);
return OnError("klonfun", refresh_proxy: true);
var selectedVoiceForSeason = voicesForSeason
.FirstOrDefault(v => !string.IsNullOrWhiteSpace(t) && v.Key.Equals(t, StringComparison.OrdinalIgnoreCase))
@ -149,12 +151,12 @@ namespace KlonFUN.Controllers
var voiceTpl = new VoiceTpl(voicesForSeason.Count);
foreach (var voice in voicesForSeason)
{
string voiceLink = $"{host}/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.Key)}&href={HttpUtility.UrlEncode(itemUrl)}";
string voiceLink = $"{host}/lite/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.Key)}&href={HttpUtility.UrlEncode(itemUrl)}";
voiceTpl.Append(voice.DisplayName, voice.Key.Equals(selectedVoiceForSeason.Key, StringComparison.OrdinalIgnoreCase), voiceLink);
}
if (!selectedVoiceForSeason.Seasons.TryGetValue(s, out List<SerialEpisode> episodes) || episodes.Count == 0)
return OnError("klonfun", proxyManager);
return OnError("klonfun", refresh_proxy: true);
var episodeTpl = new EpisodeTpl(episodes.Count);
foreach (SerialEpisode episode in episodes.OrderBy(e => e.Number))
@ -177,7 +179,7 @@ namespace KlonFUN.Controllers
{
var streams = await invoke.GetMovieStreams(item.PlayerUrl);
if (streams == null || streams.Count == 0)
return OnError("klonfun", proxyManager);
return OnError("klonfun", refresh_proxy: true);
var movieTpl = new MovieTpl(contentTitle, contentOriginalTitle, streams.Count);
for (int i = 0; i < streams.Count; i++)
@ -217,6 +219,24 @@ namespace KlonFUN.Controllers
return HostStreamProxy(init, link);
}
private void TryEnableMagicApn(OnlinesSettings init)
{
if (init == null
|| init.apn != null
|| init.streamproxy
|| string.IsNullOrWhiteSpace(ModInit.MagicApnAshdiHost))
return;
string player = new RchClient(HttpContext, host, init, requestInfo).InfoConnected()?.player;
bool useInnerPlayer = string.IsNullOrWhiteSpace(player)
|| player.Equals("inner", StringComparison.OrdinalIgnoreCase);
if (!useInnerPlayer)
return;
ApnHelper.ApplyInitConf(true, ModInit.MagicApnAshdiHost, init);
OnLog($"KlonFUN: увімкнено magic_apn для Ashdi (player={player ?? "unknown"}).");
}
private static string StripLampacArgs(string url)
{
if (string.IsNullOrWhiteSpace(url))
@ -232,5 +252,38 @@ namespace KlonFUN.Controllers
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned;
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

4
KlonFUN/GlobalUsings.cs Normal file
View File

@ -0,0 +1,4 @@
global using Shared.Services;
global using Shared.Services.Hybrid;
global using Shared.Models.Base;
global using AppInit = Shared.CoreInit;

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>

View File

@ -28,13 +28,15 @@ namespace KlonFUN
private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
private readonly HttpHydra _httpHydra;
public KlonFUNInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
public KlonFUNInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_httpHydra = httpHydra;
}
public async Task<List<SearchResult>> Search(string imdbId, string title, string originalTitle)
@ -108,7 +110,7 @@ namespace KlonFUN
try
{
var headers = DefaultHeaders();
string html = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
string html = await HttpGet(url, headers);
if (string.IsNullOrWhiteSpace(html))
return null;
@ -362,7 +364,7 @@ namespace KlonFUN
var headers = DefaultHeaders();
string form = $"do=search&subaction=search&story={HttpUtility.UrlEncode(query)}";
string html = await Http.Post(_init.cors(_init.host), form, headers: headers, proxy: _proxyManager.Get());
string html = await HttpPost(_init.host, form, headers);
if (string.IsNullOrWhiteSpace(html))
return null;
@ -465,7 +467,7 @@ namespace KlonFUN
requestUrl = ApnHelper.WrapUrl(_init, playerUrl);
var headers = DefaultHeaders();
return await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
return await HttpGet(requestUrl, headers);
}
private static JArray ParsePlayerArray(string html)
@ -699,16 +701,28 @@ namespace KlonFUN
return null;
}
private Task<string> HttpGet(string url, List<HeadersModel> headers)
{
if (_httpHydra != null)
return _httpHydra.Get(url, newheaders: headers);
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
}
private Task<string> HttpPost(string url, string data, List<HeadersModel> headers)
{
if (_httpHydra != null)
return _httpHydra.Post(url, data, newheaders: headers);
return Http.Post(_init.cors(url), data, headers: headers, proxy: _proxyManager.Get());
}
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;
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;

View File

@ -3,6 +3,7 @@ using Shared;
using Shared.Engine;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System;
@ -13,17 +14,19 @@ using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Shared.Models.Events;
namespace KlonFUN
{
public class ModInit
public class ModInit : IModuleLoaded
{
public static double Version => 1.1;
public static double Version => 2.0;
public static OnlinesSettings KlonFUN;
public static ModuleConfig KlonFUN;
public static bool ApnHostProvided;
public static string MagicApnAshdiHost;
public static OnlinesSettings Settings
public static ModuleConfig Settings
{
get => KlonFUN;
set => KlonFUN = value;
@ -32,9 +35,18 @@ namespace KlonFUN
/// <summary>
/// Модуль завантажено.
/// </summary>
public static void loaded(InitspaceModel initspace)
public void Loaded(InitspaceModel initspace)
{
KlonFUN = new OnlinesSettings("KlonFUN", "https://klon.fun", streamproxy: false, useproxy: false)
UpdateConfig();
EventListener.UpdateInitFile += UpdateConfig;
// Додаємо підтримку "уточнити пошук".
RegisterWithSearch("klonfun");
}
private void UpdateConfig()
{
KlonFUN = new ModuleConfig("KlonFUN", "https://klon.fun", streamproxy: false, useproxy: false)
{
displayname = "KlonFUN",
displayindex = 0,
@ -47,16 +59,24 @@ namespace KlonFUN
}
};
var conf = ModuleInvoke.Conf("KlonFUN", KlonFUN);
var defaults = JObject.FromObject(KlonFUN);
defaults["magic_apn"] = new JObject()
{
["ashdi"] = ApnHelper.DefaultHost
};
var conf = ModuleInvoke.Init("KlonFUN", defaults) ?? defaults;
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
MagicApnAshdiHost = ApnHelper.TryGetMagicAshdiHost(conf);
conf.Remove("magic_apn");
conf.Remove("apn");
conf.Remove("apn_host");
KlonFUN = conf.ToObject<OnlinesSettings>();
KlonFUN = conf.ToObject<ModuleConfig>();
if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, KlonFUN);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
ApnHostProvided = ApnHelper.IsEnabled(KlonFUN);
if (hasApn && apnEnabled)
if (ApnHostProvided)
{
KlonFUN.streamproxy = false;
}
@ -65,9 +85,45 @@ namespace KlonFUN
KlonFUN.apnstream = false;
KlonFUN.apn = null;
}
}
// Додаємо підтримку "уточнити пошук".
AppInit.conf.online.with_search.Add("klonfun");
private static void RegisterWithSearch(string plugin)
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
{
foreach (var item in list)
{
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
return;
}
list.Add(plugin);
}
}
catch
{
}
}
public void Dispose()
{
EventListener.UpdateInitFile -= UpdateConfig;
}
}

18
KlonFUN/ModuleConfig.cs Normal file
View File

@ -0,0 +1,18 @@
using Shared.Models.Online.Settings;
namespace KlonFUN
{
public class MagicApnSettings
{
public string ashdi { get; set; }
}
public class ModuleConfig : OnlinesSettings
{
public ModuleConfig(string plugin, string host, string apihost = null, bool useproxy = false, string token = null, bool enable = true, bool streamproxy = false, bool rip = false, bool forceEncryptToken = false, string rch_access = null, string stream_access = null) : base(plugin, host, apihost, useproxy, token, enable, streamproxy, rip, forceEncryptToken, rch_access, stream_access)
{
}
public MagicApnSettings magic_apn { get; set; }
}
}

View File

@ -1,37 +1,32 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace KlonFUN
{
public class OnlineApi
public class OnlineApi : IModuleOnline
{
public static List<(string name, string url, string plugin, int index)> Invoke(
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, 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)
private static List<ModuleOnlineItem> 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 online = new List<ModuleOnlineItem>();
var init = ModInit.KlonFUN;
if (init.enable && !init.rip)
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
url = $"{host}/klonfun";
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add((init.displayname, url, "klonfun", init.displayindex));
online.Add(new ModuleOnlineItem(init, "klonfun"));
}
return online;

View File

@ -1,6 +1,3 @@
{
"enable": true,
"version": 3,
"initspace": "KlonFUN.ModInit",
"online": "KlonFUN.OnlineApi"
"enable": true
}

View File

@ -23,20 +23,37 @@ namespace Shared.Engine
if (apnToken.Type == JTokenType.Boolean)
{
enabled = apnToken.Value<bool>();
host = conf.Value<string>("apn_host");
host = NormalizeHost(conf.Value<string>("apn_host"));
return true;
}
if (apnToken.Type == JTokenType.String)
{
host = apnToken.Value<string>();
enabled = !string.IsNullOrWhiteSpace(host);
host = NormalizeHost(apnToken.Value<string>());
enabled = host != null;
return true;
}
return false;
}
public static string TryGetMagicAshdiHost(JObject conf)
{
if (conf == null || !conf.TryGetValue("magic_apn", out var magicToken) || magicToken == null)
return null;
if (magicToken.Type == JTokenType.Boolean)
return magicToken.Value<bool>() ? DefaultHost : null;
if (magicToken.Type == JTokenType.String)
return NormalizeHost(magicToken.Value<string>());
if (magicToken.Type != JTokenType.Object)
return null;
return NormalizeHost(((JObject)magicToken).Value<string>("ashdi"));
}
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
{
if (init == null)
@ -49,8 +66,13 @@ namespace Shared.Engine
return;
}
if (string.IsNullOrWhiteSpace(host))
host = DefaultHost;
host = NormalizeHost(host);
if (host == null)
{
init.apnstream = false;
init.apn = null;
return;
}
if (init.apn == null)
init.apn = new ApnConf();
@ -94,5 +116,13 @@ namespace Shared.Engine
return $"{host.TrimEnd('/')}/{url}";
}
private static string NormalizeHost(string host)
{
if (string.IsNullOrWhiteSpace(host))
return null;
return host.Trim();
}
}
}

View File

@ -13,7 +13,7 @@ using Makhno.Models;
namespace Makhno
{
[Route("makhno")]
[Route("lite/makhno")]
public class MakhnoController : BaseOnlineController
{
private readonly ProxyManager proxyManager;
@ -28,7 +28,7 @@ namespace Makhno
{
if (checksearch)
{
if (AppInit.conf?.online?.checkOnlineSearch != true)
if (!IsCheckOnlineSearchEnabled())
return OnError();
return Content("data-json=", "text/plain; charset=utf-8");
@ -36,14 +36,15 @@ namespace Makhno
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Makhno);
var init = loadKit(ModInit.Makhno);
if (!init.enable)
return OnError();
TryEnableMagicApn(init);
Initialization(init);
OnLog($"Makhno: {title} (serial={serial}, s={s}, season={season}, t={t})");
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager);
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
var resolved = await ResolvePlaySource(imdb_id, serial, invoke);
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
@ -61,14 +62,15 @@ namespace Makhno
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Makhno);
var init = loadKit(ModInit.Makhno);
if (!init.enable)
return OnError();
TryEnableMagicApn(init);
Initialization(init);
OnLog($"Makhno Play: {title} (s={s}, season={season}, t={t}, episodeId={episodeId}) play={play}");
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager);
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
var resolved = await ResolvePlaySource(imdb_id, serial: 1, invoke);
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
return OnError();
@ -119,14 +121,15 @@ namespace Makhno
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Makhno);
var init = loadKit(ModInit.Makhno);
if (!init.enable)
return OnError();
TryEnableMagicApn(init);
Initialization(init);
OnLog($"Makhno PlayMovie: {title} ({year}) play={play}");
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager);
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
var resolved = await ResolvePlaySource(imdb_id, serial: 0, invoke);
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
return OnError();
@ -267,7 +270,7 @@ namespace Makhno
string voiceParam = seasonVoiceIndex.HasValue ? $"&t={seasonVoiceIndex.Value}" : string.Empty;
string seasonName = seasonItem.HasValue ? seasonItem.Value.Season?.Title ?? $"Сезон {seasonNumber}" : $"Сезон {seasonNumber}";
string link = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}{voiceParam}";
string link = $"{host}/lite/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}{voiceParam}";
season_tpl.Append(seasonName, link, seasonNumber.ToString());
}
@ -337,7 +340,7 @@ namespace Makhno
string voiceParam = seasonVoiceIndexForTpl.HasValue ? $"&t={seasonVoiceIndexForTpl.Value}" : string.Empty;
string seasonName = seasonItem.HasValue ? seasonItem.Value.Season?.Title ?? $"Сезон {seasonNumber}" : $"Сезон {seasonNumber}";
string link = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}{voiceParam}";
string link = $"{host}/lite/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}{voiceParam}";
seasonTplForVoice.Append(seasonName, link, seasonNumber.ToString());
}
@ -354,11 +357,11 @@ namespace Makhno
bool sameSeasonSet = seasonsForVoice.Select(s => s.Number).ToHashSet().SetEquals(selectedVoiceSeasonSet);
if (hasRequestedSeason && sameSeasonSet)
{
voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={requestedSeason}&t={i}";
voiceLink = $"{host}/lite/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={requestedSeason}&t={i}";
}
else
{
voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season=-1&t={i}";
voiceLink = $"{host}/lite/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season=-1&t={i}";
}
bool isActive = selectedVoice == i.ToString();
@ -374,7 +377,7 @@ namespace Makhno
bool hasRequestedSeason = seasonsForVoice.Any(s => s.Number == requestedSeason);
if (!hasRequestedSeason)
{
string redirectUrl = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season=-1&t={voiceIndex}";
string redirectUrl = $"{host}/lite/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season=-1&t={voiceIndex}";
return UpdateService.Validate(Redirect(redirectUrl));
}
@ -512,10 +515,61 @@ namespace Makhno
return HostStreamProxy(init, link);
}
private void TryEnableMagicApn(OnlinesSettings init)
{
if (init == null
|| init.apn != null
|| init.streamproxy
|| string.IsNullOrWhiteSpace(ModInit.MagicApnAshdiHost))
return;
string player = new RchClient(HttpContext, host, init, requestInfo).InfoConnected()?.player;
bool useInnerPlayer = string.IsNullOrWhiteSpace(player)
|| player.Equals("inner", StringComparison.OrdinalIgnoreCase);
if (!useInnerPlayer)
return;
ApnHelper.ApplyInitConf(true, ModInit.MagicApnAshdiHost, init);
OnLog($"Makhno: увімкнено magic_apn для Ashdi (player={player ?? "unknown"}).");
}
private class ResolveResult
{
public string PlayUrl { get; set; }
public bool IsSerial { get; set; }
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

4
Makhno/GlobalUsings.cs Normal file
View File

@ -0,0 +1,4 @@
global using Shared.Services;
global using Shared.Services.Hybrid;
global using Shared.Models.Base;
global using AppInit = Shared.CoreInit;

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>

View File

@ -24,13 +24,15 @@ namespace Makhno
private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
private readonly HttpHydra _httpHydra;
public MakhnoInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
public MakhnoInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_httpHydra = httpHydra;
}
public async Task<string> GetWormholePlay(string imdbId)
@ -46,7 +48,7 @@ namespace Makhno
new HeadersModel("User-Agent", Http.UserAgent)
};
string response = await Http.Get(_init.cors(url), timeoutSeconds: 4, headers: headers, proxy: _proxyManager.Get());
string response = await HttpGet(url, headers, timeoutSeconds: 4);
if (string.IsNullOrWhiteSpace(response))
return null;
@ -84,7 +86,7 @@ namespace Makhno
_onLog($"Makhno getting player data from: {requestUrl}");
var response = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
var response = await HttpGet(requestUrl, headers);
if (string.IsNullOrEmpty(response))
return null;
@ -526,6 +528,14 @@ namespace Makhno
return normalized;
}
private Task<string> HttpGet(string url, List<HeadersModel> headers, int timeoutSeconds = 15)
{
if (_httpHydra != null)
return _httpHydra.Get(url, newheaders: headers);
return Http.Get(_init.cors(url), timeoutSeconds: timeoutSeconds, headers: headers, proxy: _proxyManager.Get());
}
private class WormholeResponse
{
public string play { get; set; }

View File

@ -4,6 +4,7 @@ using Shared.Engine;
using Newtonsoft.Json.Linq;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Caching.Memory;
@ -21,12 +22,13 @@ using System.Threading.Tasks;
namespace Makhno
{
public class ModInit
public class ModInit : IModuleLoaded
{
public static double Version => 2.1;
public static double Version => 3.0;
public static OnlinesSettings Makhno;
public static bool ApnHostProvided;
public static string MagicApnAshdiHost;
public static OnlinesSettings Settings
{
@ -37,7 +39,7 @@ namespace Makhno
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
public void Loaded(InitspaceModel initspace)
{
Makhno = new OnlinesSettings("Makhno", "https://wh.lme.isroot.in", streamproxy: false, useproxy: false)
{
@ -51,8 +53,16 @@ namespace Makhno
list = new string[] { "socks5://ip:port" }
}
};
var conf = ModuleInvoke.Conf("Makhno", Makhno);
var defaults = JObject.FromObject(Makhno);
defaults["magic_apn"] = new JObject()
{
["ashdi"] = ApnHelper.DefaultHost
};
var conf = ModuleInvoke.Init("Makhno", defaults) ?? defaults;
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
MagicApnAshdiHost = ApnHelper.TryGetMagicAshdiHost(conf);
conf.Remove("magic_apn");
if (hasApn)
{
conf.Remove("apn");
@ -61,8 +71,8 @@ namespace Makhno
Makhno = conf.ToObject<OnlinesSettings>();
if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, Makhno);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
if (hasApn && apnEnabled)
ApnHostProvided = ApnHelper.IsEnabled(Makhno);
if (ApnHostProvided)
{
Makhno.streamproxy = false;
}
@ -73,7 +83,45 @@ namespace Makhno
}
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("makhno");
RegisterWithSearch("makhno");
}
private static void RegisterWithSearch(string plugin)
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
{
foreach (var item in list)
{
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
return;
}
list.Add(plugin);
}
}
catch
{
}
}
public void Dispose()
{
}
}

View File

@ -1,37 +1,32 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Makhno
{
public class OnlineApi
public class OnlineApi : IModuleOnline
{
public static List<(string name, string url, string plugin, int index)> Invoke(
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, 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)
private static List<ModuleOnlineItem> 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 online = new List<ModuleOnlineItem>();
var init = ModInit.Makhno;
if (init.enable && !init.rip)
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
url = $"{host}/makhno";
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add((init.displayname, url, "makhno", init.displayindex));
online.Add(new ModuleOnlineItem(init, "makhno"));
}
return online;

View File

@ -1,6 +1,3 @@
{
"enable": true,
"version": 3,
"initspace": "Makhno.ModInit",
"online": "Makhno.OnlineApi"
"enable": true
}

View File

@ -25,6 +25,23 @@ namespace Shared.Engine
return true;
}
public static string TryGetMagicAshdiHost(JObject conf)
{
if (conf == null || !conf.TryGetValue("magic_apn", out var magicToken) || magicToken == null)
return null;
if (magicToken.Type == JTokenType.Boolean)
return magicToken.Value<bool>() ? DefaultHost : null;
if (magicToken.Type == JTokenType.String)
return NormalizeHost(magicToken.Value<string>());
if (magicToken.Type != JTokenType.Object)
return null;
return NormalizeHost(((JObject)magicToken).Value<string>("ashdi"));
}
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
{
if (init == null)
@ -37,8 +54,13 @@ namespace Shared.Engine
return;
}
if (string.IsNullOrWhiteSpace(host))
host = DefaultHost;
host = NormalizeHost(host);
if (host == null)
{
init.apnstream = false;
init.apn = null;
return;
}
if (init.apn == null)
init.apn = new ApnConf();
@ -82,5 +104,13 @@ namespace Shared.Engine
return $"{host.TrimEnd('/')}/{url}";
}
private static string NormalizeHost(string host)
{
if (string.IsNullOrWhiteSpace(host))
return null;
return host.Trim();
}
}
}

View File

@ -23,48 +23,49 @@ namespace Mikai.Controllers
}
[HttpGet]
[Route("mikai")]
[Route("lite/mikai")]
public async Task<ActionResult> 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 t, int s = -1, bool rjson = false, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Mikai);
var init = loadKit(ModInit.Mikai);
if (!init.enable)
return Forbid();
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager);
TryEnableMagicApn(init);
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager, httpHydra);
if (checksearch)
{
if (AppInit.conf?.online?.checkOnlineSearch != true)
return OnError("mikai", _proxyManager);
if (!IsCheckOnlineSearchEnabled())
return OnError("mikai", refresh_proxy: true);
var checkResults = await invoke.Search(title, original_title, year);
if (checkResults != null && checkResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8");
return OnError("mikai", _proxyManager);
return OnError("mikai", refresh_proxy: true);
}
OnLog($"Mikai Index: title={title}, original_title={original_title}, serial={serial}, s={s}, t={t}, year={year}");
var searchResults = await invoke.Search(title, original_title, year);
if (searchResults == null || searchResults.Count == 0)
return OnError("mikai", _proxyManager);
return OnError("mikai", refresh_proxy: true);
var selected = searchResults.FirstOrDefault();
if (selected == null)
return OnError("mikai", _proxyManager);
return OnError("mikai", refresh_proxy: true);
var details = await invoke.GetDetails(selected.Id);
if (details == null || details.Players == null || details.Players.Count == 0)
return OnError("mikai", _proxyManager);
return OnError("mikai", refresh_proxy: true);
bool isSerial = serial == 1 || (serial == -1 && !string.Equals(details.Format, "movie", StringComparison.OrdinalIgnoreCase));
var seasonDetails = await CollectSeasonDetails(details, invoke);
var voices = BuildVoices(seasonDetails);
if (voices.Count == 0)
return OnError("mikai", _proxyManager);
return OnError("mikai", refresh_proxy: true);
string displayTitle = title ?? details.Details?.Names?.Name ?? original_title;
@ -81,14 +82,14 @@ namespace Mikai.Controllers
.ToList();
if (seasonNumbers.Count == 0)
return OnError("mikai", _proxyManager);
return OnError("mikai", refresh_proxy: true);
if (s == -1)
{
var seasonTpl = new SeasonTpl(seasonNumbers.Count);
foreach (var seasonNumber in seasonNumbers)
{
string link = $"{host}/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}";
string link = $"{host}/lite/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}";
if (restrictByVoice)
link += $"&t={HttpUtility.UrlEncode(t)}";
seasonTpl.Append($"{seasonNumber}", link, seasonNumber.ToString());
@ -104,7 +105,7 @@ namespace Mikai.Controllers
.ToList();
if (!voicesForSeason.Any())
return OnError("mikai", _proxyManager);
return OnError("mikai", refresh_proxy: true);
if (string.IsNullOrEmpty(t))
t = voicesForSeason[0].Key;
@ -118,7 +119,7 @@ namespace Mikai.Controllers
{
var targetSeasonSet = GetSeasonSet(voice.Value);
bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet);
string voiceLink = $"{host}/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1";
string voiceLink = $"{host}/lite/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1";
if (sameSeasonSet)
voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
else
@ -128,7 +129,7 @@ namespace Mikai.Controllers
if (!voices.ContainsKey(t) || !voices[t].Seasons.ContainsKey(s))
{
string redirectUrl = $"{host}/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s=-1&t={HttpUtility.UrlEncode(t)}";
string redirectUrl = $"{host}/lite/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s=-1&t={HttpUtility.UrlEncode(t)}";
return Redirect(redirectUrl);
}
@ -143,7 +144,7 @@ namespace Mikai.Controllers
if (NeedsResolve(voices[t].ProviderName, streamLink))
{
string callUrl = $"{host}/mikai/play?url={HttpUtility.UrlEncode(streamLink)}&title={HttpUtility.UrlEncode(displayTitle)}&serial=1";
string callUrl = $"{host}/lite/mikai/play?url={HttpUtility.UrlEncode(streamLink)}&title={HttpUtility.UrlEncode(displayTitle)}&serial=1";
episodeTpl.Append(episodeName, displayTitle, s.ToString(), ep.Number.ToString(), accsArgs(callUrl), "call");
}
else
@ -177,14 +178,14 @@ namespace Mikai.Controllers
foreach (var ashdiStream in ashdiStreams)
{
string optionName = $"{voice.DisplayName} {ashdiStream.title}";
string ashdiCallUrl = $"{host}/mikai/play?url={HttpUtility.UrlEncode(ashdiStream.link)}&title={HttpUtility.UrlEncode(displayTitle)}";
string ashdiCallUrl = $"{host}/lite/mikai/play?url={HttpUtility.UrlEncode(ashdiStream.link)}&title={HttpUtility.UrlEncode(displayTitle)}";
movieTpl.Append(optionName, accsArgs(ashdiCallUrl), "call");
}
continue;
}
}
string callUrl = $"{host}/mikai/play?url={HttpUtility.UrlEncode(episode.Url)}&title={HttpUtility.UrlEncode(displayTitle)}";
string callUrl = $"{host}/lite/mikai/play?url={HttpUtility.UrlEncode(episode.Url)}&title={HttpUtility.UrlEncode(displayTitle)}";
movieTpl.Append(voice.DisplayName, accsArgs(callUrl), "call");
}
else
@ -195,31 +196,32 @@ namespace Mikai.Controllers
}
if (movieTpl.data == null || movieTpl.data.Count == 0)
return OnError("mikai", _proxyManager);
return OnError("mikai", refresh_proxy: true);
return rjson
? Content(movieTpl.ToJson(), "application/json; charset=utf-8")
: Content(movieTpl.ToHtml(), "text/html; charset=utf-8");
}
[HttpGet("mikai/play")]
[HttpGet("lite/mikai/play")]
public async Task<ActionResult> Play(string url, string title = null, int serial = 0)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Mikai);
var init = loadKit(ModInit.Mikai);
if (!init.enable)
return Forbid();
TryEnableMagicApn(init);
if (string.IsNullOrEmpty(url))
return OnError("mikai", _proxyManager);
return OnError("mikai", refresh_proxy: true);
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager);
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager, httpHydra);
OnLog($"Mikai Play: url={url}, serial={serial}");
string streamLink = await invoke.ResolveVideoUrl(url, serial == 1);
if (string.IsNullOrEmpty(streamLink))
return OnError("mikai", _proxyManager);
return OnError("mikai", refresh_proxy: true);
List<HeadersModel> streamHeaders = null;
bool forceProxy = false;
@ -462,5 +464,56 @@ namespace Mikai.Controllers
return HostStreamProxy(init, link, headers: headers, force_streamproxy: forceProxy);
}
private void TryEnableMagicApn(OnlinesSettings init)
{
if (init == null
|| init.apn != null
|| init.streamproxy
|| string.IsNullOrWhiteSpace(ModInit.MagicApnAshdiHost))
return;
string player = new RchClient(HttpContext, host, init, requestInfo).InfoConnected()?.player;
bool useInnerPlayer = string.IsNullOrWhiteSpace(player)
|| player.Equals("inner", StringComparison.OrdinalIgnoreCase);
if (!useInnerPlayer)
return;
ApnHelper.ApplyInitConf(true, ModInit.MagicApnAshdiHost, init);
OnLog($"Mikai: увімкнено magic_apn для Ashdi (player={player ?? "unknown"}).");
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

4
Mikai/GlobalUsings.cs Normal file
View File

@ -0,0 +1,4 @@
global using Shared.Services;
global using Shared.Services.Hybrid;
global using Shared.Models.Base;
global using AppInit = Shared.CoreInit;

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>

View File

@ -23,13 +23,15 @@ namespace Mikai
private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
private readonly HttpHydra _httpHydra;
public MikaiInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
public MikaiInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_httpHydra = httpHydra;
}
public async Task<List<MikaiAnime>> Search(string title, string original_title, int year)
@ -49,7 +51,7 @@ namespace Mikai
var headers = DefaultHeaders();
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {searchUrl}");
string json = await Http.Get(_init.cors(searchUrl), headers: headers, proxy: _proxyManager.Get());
string json = await HttpGet(searchUrl, headers);
if (string.IsNullOrEmpty(json))
return null;
@ -93,7 +95,7 @@ namespace Mikai
var headers = DefaultHeaders();
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {url}");
string json = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
string json = await HttpGet(url, headers);
if (string.IsNullOrEmpty(json))
return null;
@ -144,7 +146,7 @@ namespace Mikai
};
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
string html = await HttpGet(requestUrl, headers);
if (string.IsNullOrEmpty(html))
return null;
@ -190,7 +192,7 @@ namespace Mikai
string requestUrl = AshdiRequestUrl(WithAshdiMultivoice(url, enable: !disableAshdiMultivoiceForVod));
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
string html = await HttpGet(requestUrl, headers);
if (string.IsNullOrEmpty(html))
return streams;
@ -415,12 +417,20 @@ namespace Mikai
return null;
}
private Task<string> HttpGet(string url, List<HeadersModel> headers)
{
if (_httpHydra != null)
return _httpHydra.Get(url, newheaders: headers);
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
}
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;
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;

View File

@ -3,6 +3,7 @@ using Shared;
using Shared.Engine;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Caching.Memory;
@ -22,12 +23,13 @@ using System.Threading.Tasks;
namespace Mikai
{
public class ModInit
public class ModInit : IModuleLoaded
{
public static double Version => 3.8;
public static double Version => 4.0;
public static OnlinesSettings Mikai;
public static bool ApnHostProvided;
public static string MagicApnAshdiHost;
public static OnlinesSettings Settings
{
@ -38,7 +40,7 @@ namespace Mikai
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
public void Loaded(InitspaceModel initspace)
{
@ -56,15 +58,23 @@ namespace Mikai
}
};
var conf = ModuleInvoke.Conf("Mikai", Mikai);
var defaults = JObject.FromObject(Mikai);
defaults["magic_apn"] = new JObject()
{
["ashdi"] = ApnHelper.DefaultHost
};
var conf = ModuleInvoke.Init("Mikai", defaults) ?? defaults;
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
MagicApnAshdiHost = ApnHelper.TryGetMagicAshdiHost(conf);
conf.Remove("magic_apn");
conf.Remove("apn");
conf.Remove("apn_host");
Mikai = conf.ToObject<OnlinesSettings>();
if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, Mikai);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
if (hasApn && apnEnabled)
ApnHostProvided = ApnHelper.IsEnabled(Mikai);
if (ApnHostProvided)
{
Mikai.streamproxy = false;
}
@ -75,7 +85,45 @@ namespace Mikai
}
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("mikai");
RegisterWithSearch("mikai");
}
private static void RegisterWithSearch(string plugin)
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
{
foreach (var item in list)
{
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
return;
}
list.Add(plugin);
}
}
catch
{
}
}
public void Dispose()
{
}
}
@ -197,4 +245,4 @@ namespace Mikai
}
public record ConnectResponse(bool IsUpdateUnavailable, bool IsNoiseEnabled);
}
}

View File

@ -1,47 +1,36 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Mikai
{
public class OnlineApi
public class OnlineApi : IModuleOnline
{
public static List<(string name, string url, string plugin, int index)> Invoke(
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, 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)
private static List<ModuleOnlineItem> 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 online = new List<ModuleOnlineItem>();
var init = ModInit.Mikai;
// Визначаємо isAnime згідно стандарту Lampac (Deepwiki):
// isanime = true якщо original_language == "ja" або "zh"
bool hasLang = !string.IsNullOrEmpty(original_language);
bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
// Mikai — аніме-провайдер. Додаємо його:
// - при загальному пошуку (serial == -1), або
// - якщо контент визначений як аніме (isanime), або
// - якщо мова невідома (відсутній original_language)
if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang))
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
url = $"{host}/mikai";
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add((init.displayname, url, "mikai", init.displayindex));
online.Add(new ModuleOnlineItem(init, "mikai"));
}
return online;

View File

@ -1,6 +1,3 @@
{
"enable": true,
"version": 3,
"initspace": "Mikai.ModInit",
"online": "Mikai.OnlineApi"
"enable": true
}

View File

@ -25,35 +25,35 @@ namespace NMoonAnime.Controllers
}
[HttpGet]
[Route("nmoonanime")]
[Route("lite/nmoonanime")]
public async Task<ActionResult> 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.NMoonAnime);
var init = loadKit(ModInit.NMoonAnime);
if (!init.enable)
return Forbid();
var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager);
var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
string effectiveMalId = ResolveMalId(mal_id, kinopoisk_id, source);
if (checksearch)
{
if (AppInit.conf?.online?.checkOnlineSearch != true)
return OnError("nmoonanime", proxyManager);
if (!IsCheckOnlineSearchEnabled())
return OnError("nmoonanime", refresh_proxy: true);
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);
return OnError("nmoonanime", refresh_proxy: true);
}
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)
return OnError("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
bool isSeries = serial == 1;
NMoonAnimeSeasonContent firstSeasonData = null;
@ -62,7 +62,7 @@ namespace NMoonAnime.Controllers
{
firstSeasonData = await invoke.GetSeasonContent(seasons[0]);
if (firstSeasonData == null || firstSeasonData.Voices.Count == 0)
return OnError("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
isSeries = firstSeasonData.IsSeries;
}
@ -75,22 +75,22 @@ namespace NMoonAnime.Controllers
return await RenderMovie(invoke, seasons, title, original_title, firstSeasonData, rjson);
}
[HttpGet("nmoonanime/play")]
[HttpGet("lite/nmoonanime/play")]
public async Task<ActionResult> Play(string file, string title = null)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.NMoonAnime);
var init = loadKit(ModInit.NMoonAnime);
if (!init.enable)
return Forbid();
if (string.IsNullOrWhiteSpace(file))
return OnError("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager);
var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
var streams = invoke.ParseStreams(file);
if (streams == null || streams.Count == 0)
return OnError("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
if (streams.Count == 1)
{
@ -107,7 +107,7 @@ namespace NMoonAnime.Controllers
}
if (!streamQuality.Any())
return OnError("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
var first = streamQuality.Firts();
string json = VideoTpl.ToJson("play", first.link, title ?? string.Empty, streamquality: streamQuality);
@ -133,7 +133,7 @@ namespace NMoonAnime.Controllers
.ToList();
if (orderedSeasons.Count == 0)
return OnError("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
if (selectedSeason == -1)
{
@ -154,14 +154,14 @@ namespace NMoonAnime.Controllers
var currentSeason = orderedSeasons.FirstOrDefault(s => s.SeasonNumber == selectedSeason) ?? orderedSeasons[0];
var seasonData = await invoke.GetSeasonContent(currentSeason);
if (seasonData == null)
return OnError("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
var voices = seasonData.Voices
.Where(v => v != null && v.Episodes != null && v.Episodes.Count > 0)
.ToList();
if (voices.Count == 0)
return OnError("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
int activeVoiceIndex = ParseVoiceIndex(selectedVoice, voices.Count);
var voiceTpl = new VoiceTpl(voices.Count);
@ -180,7 +180,7 @@ namespace NMoonAnime.Controllers
.ToList();
if (episodes.Count == 0)
return OnError("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
string displayTitle = !string.IsNullOrWhiteSpace(title)
? title
@ -193,7 +193,7 @@ namespace NMoonAnime.Controllers
{
int episodeNumber = episode.Number <= 0 ? 1 : episode.Number;
string episodeName = string.IsNullOrWhiteSpace(episode.Name) ? $"Епізод {episodeNumber}" : episode.Name;
string callUrl = $"{host}/nmoonanime/play?file={HttpUtility.UrlEncode(episode.File)}&title={HttpUtility.UrlEncode(displayTitle)}";
string callUrl = $"{host}/lite/nmoonanime/play?file={HttpUtility.UrlEncode(episode.File)}&title={HttpUtility.UrlEncode(displayTitle)}";
episodeTpl.Append(episodeName, displayTitle, currentSeason.SeasonNumber.ToString(), episodeNumber.ToString(), accsArgs(callUrl), "call");
}
@ -218,14 +218,14 @@ namespace NMoonAnime.Controllers
.FirstOrDefault();
if (currentSeason == null)
return OnError("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
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("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
string displayTitle = !string.IsNullOrWhiteSpace(title)
? title
@ -249,13 +249,13 @@ namespace NMoonAnime.Controllers
continue;
string voiceName = string.IsNullOrWhiteSpace(voice.Name) ? $"Озвучка {fallbackIndex}" : voice.Name;
string callUrl = $"{host}/nmoonanime/play?file={HttpUtility.UrlEncode(file)}&title={HttpUtility.UrlEncode(displayTitle)}";
string callUrl = $"{host}/lite/nmoonanime/play?file={HttpUtility.UrlEncode(file)}&title={HttpUtility.UrlEncode(displayTitle)}";
movieTpl.Append(voiceName, accsArgs(callUrl), "call");
fallbackIndex++;
}
if (movieTpl.IsEmpty)
return OnError("nmoonanime", proxyManager);
return OnError("nmoonanime", refresh_proxy: true);
return rjson
? Content(movieTpl.ToJson(), "application/json; charset=utf-8")
@ -265,7 +265,7 @@ namespace NMoonAnime.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}/nmoonanime?imdb_id={HttpUtility.UrlEncode(imdbId)}");
url.Append($"{host}/lite/nmoonanime?imdb_id={HttpUtility.UrlEncode(imdbId)}");
url.Append($"&kinopoisk_id={kinopoiskId}");
url.Append($"&title={HttpUtility.UrlEncode(title)}");
url.Append($"&original_title={HttpUtility.UrlEncode(originalTitle)}");
@ -353,5 +353,38 @@ namespace NMoonAnime.Controllers
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned;
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

View File

@ -0,0 +1,4 @@
global using Shared.Services;
global using Shared.Services.Hybrid;
global using Shared.Models.Base;
global using AppInit = Shared.CoreInit;

View File

@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
using Shared;
using Shared.Engine;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Shared.Models.Online.Settings;
using System;
using System.Net.Http;
@ -16,9 +17,9 @@ using System.Threading.Tasks;
namespace NMoonAnime
{
public class ModInit
public class ModInit : IModuleLoaded
{
public static double Version => 1.0;
public static double Version => 2.0;
public static OnlinesSettings NMoonAnime;
@ -33,7 +34,7 @@ namespace NMoonAnime
/// <summary>
/// Модуль завантажено.
/// </summary>
public static void loaded(InitspaceModel initspace)
public void Loaded(InitspaceModel initspace)
{
NMoonAnime = new OnlinesSettings("NMoonAnime", "https://moonanime.art", "https://apx.lme.isroot.in", streamproxy: false, useproxy: false)
{
@ -48,7 +49,7 @@ namespace NMoonAnime
}
};
var conf = ModuleInvoke.Conf("NMoonAnime", NMoonAnime) ?? JObject.FromObject(NMoonAnime);
var conf = ModuleInvoke.Init("NMoonAnime", JObject.FromObject(NMoonAnime)) ?? JObject.FromObject(NMoonAnime);
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
conf.Remove("apn");
conf.Remove("apn_host");
@ -68,7 +69,45 @@ namespace NMoonAnime
NMoonAnime.apn = null;
}
AppInit.conf.online.with_search.Add("nmoonanime");
RegisterWithSearch("nmoonanime");
}
private static void RegisterWithSearch(string plugin)
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
{
foreach (var item in list)
{
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
return;
}
list.Add(plugin);
}
}
catch
{
}
}
public void Dispose()
{
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>

View File

@ -22,6 +22,7 @@ namespace NMoonAnime
private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
private readonly HttpHydra _httpHydra;
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
@ -35,12 +36,13 @@ namespace NMoonAnime
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)
public NMoonAnimeInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_httpHydra = httpHydra;
}
public async Task<List<NMoonAnimeSeasonRef>> Search(string imdbId, string malId, string title, int year)
@ -64,7 +66,7 @@ namespace NMoonAnime
continue;
_onLog($"NMoonAnime: пошук через {searchUrl}");
string json = await Http.Get(_init.cors(searchUrl), headers: DefaultHeaders(), proxy: _proxyManager.Get());
string json = await HttpGet(searchUrl, DefaultHeaders());
if (string.IsNullOrWhiteSpace(json))
continue;
@ -108,7 +110,7 @@ namespace NMoonAnime
try
{
_onLog($"NMoonAnime: завантаження сезону {season.Url}");
string html = await Http.Get(_init.cors(season.Url), headers: DefaultHeaders(), proxy: _proxyManager.Get());
string html = await HttpGet(season.Url, DefaultHeaders());
if (string.IsNullOrWhiteSpace(html))
return null;
@ -1098,12 +1100,20 @@ namespace NMoonAnime
};
}
private Task<string> HttpGet(string url, List<HeadersModel> headers)
{
if (_httpHydra != null)
return _httpHydra.Get(url, newheaders: headers);
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
}
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;
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;

View File

@ -1,28 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace NMoonAnime
{
public class OnlineApi
public class OnlineApi : IModuleOnline
{
public static List<(string name, string url, string plugin, int index)> Invoke(
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, 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)
private static List<ModuleOnlineItem> 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 online = new List<ModuleOnlineItem>();
var init = ModInit.NMoonAnime;
@ -31,11 +27,10 @@ namespace NMoonAnime
if (init.enable && !init.rip && (serial == -1 || isAnime || !hasLang))
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
url = $"{host}/nmoonanime";
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add((init.displayname, url, "nmoonanime", init.displayindex));
online.Add(new ModuleOnlineItem(init, "nmoonanime"));
}
return online;

View File

@ -1,6 +1,3 @@
{
"enable": true,
"version": 3,
"initspace": "NMoonAnime.ModInit",
"online": "NMoonAnime.OnlineApi"
"enable": true
}

View File

@ -1,4 +1,4 @@
# Ukraine online source for Lampac
# Ukraine online source for Lampac NextGen
## Sources
### TVShows and Movies
@ -76,15 +76,17 @@ modules - optional, if not specified, all modules from the repository will be in
]
},
"displayindex": 1,
"apn": true,
"apn_host": "domaine.com/{encodeurl}"
"magic_apn": {
"ashdi": "https://tut.im/proxy.php?url={encodeurl}"
}
}
```
Parameter compatibility:
- `webcorshost` + `useproxy`: work together (parsing via CORS host, and network output can go through a proxy with `useproxy`).
- `webcorshost` does not conflict with `streamproxy`: CORS is used for parsing, `streamproxy` is used for streaming.
- `webcorshost` does not conflict with `apn`: APN is used at the streaming stage, not for regular parsing.
- `magic_apn.ashdi` використовується тільки для Ashdi-посилань і лише коли значення непорожнє.
- `webcorshost` не конфліктує з `magic_apn`: CORS використовується для парсингу, `magic_apn` — для Ashdi-стрімінгу.
## JackTor config example (`init.conf`)

View File

@ -24,27 +24,27 @@ namespace StarLight.Controllers
}
[HttpGet]
[Route("starlight")]
[Route("lite/starlight")]
async public Task<ActionResult> 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, int s = -1, bool rjson = false, string href = null, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.StarLight);
var init = loadKit(ModInit.StarLight);
if (!init.enable)
return Forbid();
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager);
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch)
{
if (AppInit.conf?.online?.checkOnlineSearch != true)
return OnError("starlight", proxyManager);
if (!IsCheckOnlineSearchEnabled())
return OnError("starlight", refresh_proxy: true);
var searchResults = await invoke.Search(title, original_title);
if (searchResults != null && searchResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8");
return OnError("starlight", proxyManager);
return OnError("starlight", refresh_proxy: true);
}
string itemUrl = href;
@ -52,14 +52,14 @@ namespace StarLight.Controllers
{
var searchResults = await invoke.Search(title, original_title);
if (searchResults == null || searchResults.Count == 0)
return OnError("starlight", proxyManager);
return OnError("starlight", refresh_proxy: true);
if (searchResults.Count > 1)
{
var similar_tpl = new SimilarTpl(searchResults.Count);
foreach (var res in searchResults)
{
string link = $"{host}/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Href)}";
string link = $"{host}/lite/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Href)}";
similar_tpl.Append(res.Title, string.Empty, string.Empty, link, string.Empty);
}
@ -71,7 +71,7 @@ namespace StarLight.Controllers
var project = await invoke.GetProject(itemUrl);
if (project == null)
return OnError("starlight", proxyManager);
return OnError("starlight", refresh_proxy: true);
if (serial == 1 && project.Seasons.Count > 0)
{
@ -82,7 +82,7 @@ namespace StarLight.Controllers
{
var seasonInfo = project.Seasons[i];
string seasonName = string.IsNullOrEmpty(seasonInfo.Title) ? $"Сезон {i + 1}" : seasonInfo.Title;
string link = $"{host}/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={i}&href={HttpUtility.UrlEncode(itemUrl)}";
string link = $"{host}/lite/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={i}&href={HttpUtility.UrlEncode(itemUrl)}";
season_tpl.Append(seasonName, link, i.ToString());
}
@ -90,13 +90,13 @@ namespace StarLight.Controllers
}
if (s < 0 || s >= project.Seasons.Count)
return OnError("starlight", proxyManager);
return OnError("starlight", refresh_proxy: true);
var season = project.Seasons[s];
string seasonSlug = season.Slug;
var episodes = invoke.GetEpisodes(project, seasonSlug);
if (episodes == null || episodes.Count == 0)
return OnError("starlight", proxyManager);
return OnError("starlight", refresh_proxy: true);
var episode_tpl = new EpisodeTpl();
int index = 1;
@ -114,7 +114,7 @@ namespace StarLight.Controllers
continue;
string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {index}" : ep.Title;
string callUrl = $"{host}/starlight/play?hash={HttpUtility.UrlEncode(ep.Hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
string callUrl = $"{host}/lite/starlight/play?hash={HttpUtility.UrlEncode(ep.Hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
episode_tpl.Append(episodeName, title ?? original_title, seasonNumber, index.ToString("D2"), accsArgs(callUrl), "call");
index++;
}
@ -128,9 +128,9 @@ namespace StarLight.Controllers
hash = project.Episodes.FirstOrDefault(e => !string.IsNullOrEmpty(e.Hash))?.Hash;
if (string.IsNullOrEmpty(hash))
return OnError("starlight", proxyManager);
return OnError("starlight", refresh_proxy: true);
string callUrl = $"{host}/starlight/play?hash={HttpUtility.UrlEncode(hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
string callUrl = $"{host}/lite/starlight/play?hash={HttpUtility.UrlEncode(hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
var movie_tpl = new MovieTpl(title, original_title, 1);
movie_tpl.Append(string.IsNullOrEmpty(title) ? "StarLight" : title, accsArgs(callUrl), "call");
@ -139,22 +139,22 @@ namespace StarLight.Controllers
}
[HttpGet]
[Route("starlight/play")]
[Route("lite/starlight/play")]
async public Task<ActionResult> Play(string hash, string title)
{
await UpdateService.ConnectAsync(host);
if (string.IsNullOrEmpty(hash))
return OnError("starlight", proxyManager);
return OnError("starlight", refresh_proxy: true);
var init = await loadKit(ModInit.StarLight);
var init = loadKit(ModInit.StarLight);
if (!init.enable)
return Forbid();
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager);
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
var result = await invoke.ResolveStream(hash);
if (result == null || string.IsNullOrEmpty(result.Stream))
return OnError("starlight", proxyManager);
return OnError("starlight", refresh_proxy: true);
string videoTitle = title ?? result.Name ?? "";
@ -266,5 +266,38 @@ namespace StarLight.Controllers
return DateTime.TryParse(episode.Date, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt) ? dt : null;
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

View File

@ -0,0 +1,4 @@
global using Shared.Services;
global using Shared.Services.Hybrid;
global using Shared.Models.Base;
global using AppInit = Shared.CoreInit;

View File

@ -2,6 +2,7 @@ using Newtonsoft.Json.Linq;
using Shared;
using Shared.Engine;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Shared.Models.Online.Settings;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting;
@ -22,9 +23,9 @@ using System.Threading.Tasks;
namespace StarLight
{
public class ModInit
public class ModInit : IModuleLoaded
{
public static double Version => 3.3;
public static double Version => 4.0;
public static OnlinesSettings StarLight;
public static bool ApnHostProvided;
@ -38,7 +39,7 @@ namespace StarLight
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
public void Loaded(InitspaceModel initspace)
{
@ -54,7 +55,7 @@ namespace StarLight
list = new string[] { "socks5://ip:port" }
}
};
var conf = ModuleInvoke.Conf("StarLight", StarLight);
var conf = ModuleInvoke.Init("StarLight", JObject.FromObject(StarLight));
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
conf.Remove("apn");
conf.Remove("apn_host");
@ -73,7 +74,45 @@ namespace StarLight
}
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("starlight");
RegisterWithSearch("starlight");
}
private static void RegisterWithSearch(string plugin)
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
{
foreach (var item in list)
{
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
return;
}
list.Add(plugin);
}
}
catch
{
}
}
public void Dispose()
{
}
}

View File

@ -1,29 +1,25 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace StarLight
{
public class OnlineApi
public class OnlineApi : IModuleOnline
{
public static List<(string name, string url, string plugin, int index)> Invoke(
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, 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)
private static List<ModuleOnlineItem> 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 online = new List<ModuleOnlineItem>();
if (!string.Equals(original_language, "uk", StringComparison.OrdinalIgnoreCase))
return online;
@ -31,11 +27,10 @@ namespace StarLight
var init = ModInit.StarLight;
if (init.enable && !init.rip)
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
url = $"{host}/starlight";
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add((init.displayname, url, "starlight", init.displayindex));
online.Add(new ModuleOnlineItem(init, "starlight"));
}
return online;

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>

View File

@ -24,13 +24,15 @@ namespace StarLight
private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
private readonly HttpHydra _httpHydra;
public StarLightInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
public StarLightInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_httpHydra = httpHydra;
}
public async Task<List<SearchResult>> Search(string title, string original_title)
@ -54,7 +56,7 @@ namespace StarLight
try
{
_onLog?.Invoke($"StarLight search: {url}");
string payload = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
string payload = await HttpGet(url, headers);
if (string.IsNullOrEmpty(payload))
return null;
@ -112,7 +114,7 @@ namespace StarLight
try
{
_onLog?.Invoke($"StarLight project: {href}");
string payload = await Http.Get(_init.cors(href), headers: headers, proxy: _proxyManager.Get());
string payload = await HttpGet(href, headers);
if (string.IsNullOrEmpty(payload))
return null;
@ -193,7 +195,7 @@ namespace StarLight
try
{
_onLog?.Invoke($"StarLight season: {seasonUrl}");
string payload = await Http.Get(_init.cors(seasonUrl), headers: headers, proxy: _proxyManager.Get());
string payload = await HttpGet(seasonUrl, headers);
if (string.IsNullOrEmpty(payload))
continue;
@ -279,7 +281,7 @@ namespace StarLight
try
{
_onLog?.Invoke($"StarLight stream: {url}");
string payload = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
string payload = await HttpGet(url, headers);
if (string.IsNullOrEmpty(payload))
return null;
@ -338,12 +340,20 @@ namespace StarLight
return $"{_init.host}{path}";
}
private Task<string> HttpGet(string url, List<HeadersModel> headers)
{
if (_httpHydra != null)
return _httpHydra.Get(url, newheaders: headers);
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
}
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;
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;

View File

@ -1,6 +1,3 @@
{
"enable": true,
"version": 3,
"initspace": "StarLight.ModInit",
"online": "StarLight.OnlineApi"
"enable": true
}

99
UafilmME/ApnHelper.cs Normal file
View File

@ -0,0 +1,99 @@
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<bool>();
host = conf.Value<string>("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;
}
host = NormalizeHost(host);
if (host == null)
{
init.apnstream = false;
init.apn = null;
return;
}
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}";
}
private static string NormalizeHost(string host)
{
if (string.IsNullOrWhiteSpace(host))
return null;
return host.Trim();
}
}
}

356
UafilmME/Controller.cs Normal file
View File

@ -0,0 +1,356 @@
using Microsoft.AspNetCore.Mvc;
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.Threading.Tasks;
using System.Web;
using UafilmME.Models;
namespace UafilmME.Controllers
{
public class Controller : BaseOnlineController
{
ProxyManager proxyManager;
public Controller() : base(ModInit.Settings)
{
proxyManager = new ProxyManager(ModInit.UafilmME);
}
[HttpGet]
[Route("lite/uafilmme")]
async public Task<ActionResult> 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 t, int s = -1, bool rjson = false, string href = null, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = loadKit(ModInit.UafilmME);
if (!init.enable)
return Forbid();
var invoke = new UafilmMEInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch)
{
if (!IsCheckOnlineSearchEnabled())
return OnError("uafilmme", refresh_proxy: true);
var searchResults = await invoke.Search(title, original_title, year);
if (searchResults != null && searchResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8");
return OnError("uafilmme", refresh_proxy: true);
}
long titleId = 0;
long.TryParse(href, out titleId);
if (titleId <= 0)
{
var searchResults = await invoke.Search(title, original_title, year);
if (searchResults == null || searchResults.Count == 0)
{
OnLog("UafilmME: пошук нічого не повернув.");
return OnError("uafilmme", refresh_proxy: true);
}
var best = invoke.SelectBestSearchResult(searchResults, id, imdb_id, title, original_title, year, serial);
var ordered = searchResults
.OrderByDescending(r => r.MatchScore)
.ThenByDescending(r => r.Year)
.ToList();
var second = ordered.Skip(1).FirstOrDefault();
if (!IsConfidentMatch(best, second, id, imdb_id, serial))
{
var similarTpl = new SimilarTpl(ordered.Count);
foreach (var item in ordered.Take(60))
{
string details = item.IsSeries ? "Серіал" : "Фільм";
string itemYear = item.Year > 1900 ? item.Year.ToString() : string.Empty;
string link = $"{host}/lite/uafilmme?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={item.Id}";
similarTpl.Append(item.Name, itemYear, details, link, item.Poster);
}
OnLog($"UafilmME: кілька схожих збігів, повертаю SimilarTpl ({ordered.Count}).");
return rjson
? Content(similarTpl.ToJson(), "application/json; charset=utf-8")
: Content(similarTpl.ToHtml(), "text/html; charset=utf-8");
}
titleId = best?.Id ?? 0;
}
if (titleId <= 0)
{
OnLog("UafilmME: не вдалося визначити title_id.");
return OnError("uafilmme", refresh_proxy: true);
}
if (serial == 1)
{
if (s == -1)
{
var seasons = await invoke.GetAllSeasons(titleId);
if (seasons == null || seasons.Count == 0)
{
OnLog($"UafilmME: сезони не знайдено для title_id={titleId}.");
return OnError("uafilmme", refresh_proxy: true);
}
var seasonTpl = new SeasonTpl(seasons.Count);
foreach (var season in seasons)
{
string seasonName = season.EpisodesCount > 0
? $"Сезон {season.Number} ({season.EpisodesCount} еп.)"
: $"Сезон {season.Number}";
string link = $"{host}/lite/uafilmme?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season.Number}&href={titleId}";
seasonTpl.Append(seasonName, link, season.Number.ToString());
}
return rjson
? Content(seasonTpl.ToJson(), "application/json; charset=utf-8")
: Content(seasonTpl.ToHtml(), "text/html; charset=utf-8");
}
if (s <= 0)
{
OnLog($"UafilmME: некоректний номер сезону s={s}.");
return OnError("uafilmme", refresh_proxy: true);
}
var episodes = await invoke.GetSeasonEpisodes(titleId, s);
if (episodes == null || episodes.Count == 0)
{
OnLog($"UafilmME: епізоди не знайдено для title_id={titleId}, season={s}.");
return OnError("uafilmme", refresh_proxy: true);
}
var episodeTpl = new EpisodeTpl();
int appended = 0;
int fallbackEpisodeNumber = 1;
foreach (var episode in episodes)
{
if (episode.PrimaryVideoId <= 0)
continue;
int episodeNumber = episode.EpisodeNumber > 0 ? episode.EpisodeNumber : fallbackEpisodeNumber;
string episodeName = !string.IsNullOrWhiteSpace(episode.Name)
? episode.Name
: $"Епізод {episodeNumber}";
string callUrl = $"{host}/lite/uafilmme/play?video_id={episode.PrimaryVideoId}&title_id={titleId}&s={s}&e={episodeNumber}&title={HttpUtility.UrlEncode(title ?? original_title)}";
episodeTpl.Append(episodeName, title ?? original_title, s.ToString(), episodeNumber.ToString("D2"), accsArgs(callUrl), "call");
fallbackEpisodeNumber = Math.Max(fallbackEpisodeNumber, episodeNumber + 1);
appended++;
}
if (appended == 0)
{
OnLog($"UafilmME: у сезоні {s} немає епізодів з playable video_id.");
return OnError("uafilmme", refresh_proxy: true);
}
return rjson
? Content(episodeTpl.ToJson(), "application/json; charset=utf-8")
: Content(episodeTpl.ToHtml(), "text/html; charset=utf-8");
}
else
{
var videos = await invoke.GetMovieVideos(titleId);
if (videos == null || videos.Count == 0)
{
OnLog($"UafilmME: не знайдено відео для фільму title_id={titleId}.");
return OnError("uafilmme", refresh_proxy: true);
}
var movieTpl = new MovieTpl(title, original_title, videos.Count);
int index = 1;
foreach (var video in videos)
{
string label = BuildVideoLabel(video, index);
string callUrl = $"{host}/lite/uafilmme/play?video_id={video.Id}&title_id={titleId}&title={HttpUtility.UrlEncode(title ?? original_title)}";
movieTpl.Append(label, accsArgs(callUrl), "call");
index++;
}
return rjson
? Content(movieTpl.ToJson(), "application/json; charset=utf-8")
: Content(movieTpl.ToHtml(), "text/html; charset=utf-8");
}
}
[HttpGet]
[Route("lite/uafilmme/play")]
async public Task<ActionResult> Play(long video_id, long title_id = 0, int s = 0, int e = 0, string title = null)
{
await UpdateService.ConnectAsync(host);
if (video_id <= 0)
return OnError("uafilmme", refresh_proxy: true);
var init = loadKit(ModInit.UafilmME);
if (!init.enable)
return Forbid();
var invoke = new UafilmMEInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
var watch = await invoke.GetWatch(video_id);
var videos = invoke.CollectPlayableVideos(watch);
if (videos == null || videos.Count == 0)
{
OnLog($"UafilmME Play: watch/{video_id} не повернув playable stream.");
return OnError("uafilmme", refresh_proxy: true);
}
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", init.host)
};
var streamQuality = new StreamQualityTpl();
foreach (var video in videos)
{
string streamUrl = BuildStreamUrl(init, video.Src, headers, forceProxy: true);
if (string.IsNullOrWhiteSpace(streamUrl))
continue;
string label = BuildVideoLabel(video, 0);
streamQuality.Append(streamUrl, label);
}
var first = streamQuality.Firts();
if (string.IsNullOrWhiteSpace(first.link))
{
OnLog($"UafilmME Play: не вдалося зібрати streamquality для video_id={video_id}.");
return OnError("uafilmme", refresh_proxy: true);
}
string videoTitle = !string.IsNullOrWhiteSpace(title)
? title
: videos.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v.Name))?.Name ?? string.Empty;
return UpdateService.Validate(
Content(
VideoTpl.ToJson("play", first.link, videoTitle, streamquality: streamQuality),
"application/json; charset=utf-8"
)
);
}
string BuildStreamUrl(OnlinesSettings init, string streamLink, List<HeadersModel> headers, bool forceProxy)
{
string link = StripLampacArgs(streamLink?.Trim());
if (string.IsNullOrEmpty(link))
return link;
if (ApnHelper.IsEnabled(init))
{
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
return ApnHelper.WrapUrl(init, link);
var noApn = (OnlinesSettings)init.Clone();
noApn.apnstream = false;
noApn.apn = null;
return HostStreamProxy(noApn, link, headers: headers, force_streamproxy: forceProxy, proxy: proxyManager.Get());
}
return HostStreamProxy(init, link, headers: headers, force_streamproxy: forceProxy, proxy: proxyManager.Get());
}
private static bool IsConfidentMatch(UafilmSearchItem best, UafilmSearchItem second, long tmdbId, string imdbId, int serial)
{
if (best == null)
return false;
bool sameTmdb = tmdbId > 0 && best.TmdbId == tmdbId;
bool sameImdb = !string.IsNullOrWhiteSpace(imdbId)
&& !string.IsNullOrWhiteSpace(best.ImdbId)
&& string.Equals(best.ImdbId.Trim(), imdbId.Trim(), StringComparison.OrdinalIgnoreCase);
if (sameTmdb || sameImdb)
return true;
if (serial == 1 && !best.IsSeries)
return false;
int secondScore = second?.MatchScore ?? int.MinValue;
return best.MatchScore >= 65 && best.MatchScore - secondScore >= 10;
}
private static string BuildVideoLabel(UafilmVideoItem video, int index)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(video?.Name))
parts.Add(video.Name.Trim());
if (!string.IsNullOrWhiteSpace(video?.Quality))
parts.Add(video.Quality.Trim());
if (!string.IsNullOrWhiteSpace(video?.Language))
parts.Add(video.Language.Trim());
if (parts.Count == 0)
return index > 0 ? $"Варіант {index}" : "Потік";
return string.Join(" • ", parts.Distinct(StringComparer.OrdinalIgnoreCase));
}
private static string StripLampacArgs(string url)
{
if (string.IsNullOrEmpty(url))
return url;
string cleaned = System.Text.RegularExpressions.Regex.Replace(
url,
@"([?&])(account_email|uid|nws_id)=[^&]*",
"$1",
System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned;
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

4
UafilmME/GlobalUsings.cs Normal file
View File

@ -0,0 +1,4 @@
global using Shared.Services;
global using Shared.Services.Hybrid;
global using Shared.Models.Base;
global using AppInit = Shared.CoreInit;

228
UafilmME/ModInit.cs Normal file
View File

@ -0,0 +1,228 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Shared;
using Shared.Engine;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Shared.Models.Online.Settings;
using Microsoft.AspNetCore.Mvc;
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 UafilmME
{
public class ModInit : IModuleLoaded
{
public static double Version => 1.0;
public static OnlinesSettings UafilmME;
public static bool ApnHostProvided;
public static OnlinesSettings Settings
{
get => UafilmME;
set => UafilmME = value;
}
/// <summary>
/// Модуль завантажено.
/// </summary>
public void Loaded(InitspaceModel initspace)
{
UafilmME = new OnlinesSettings("UafilmME", "https://uafilm.me", streamproxy: false, useproxy: false)
{
displayname = "UAFilmME",
displayindex = 0,
proxy = new Shared.Models.Base.ProxySettings()
{
useAuth = true,
username = "",
password = "",
list = new string[] { "socks5://ip:port" }
}
};
var conf = ModuleInvoke.Init("UafilmME", JObject.FromObject(UafilmME));
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
conf.Remove("apn");
conf.Remove("apn_host");
UafilmME = conf.ToObject<OnlinesSettings>();
if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, UafilmME);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
if (hasApn && apnEnabled)
{
UafilmME.streamproxy = false;
}
else if (UafilmME.streamproxy)
{
UafilmME.apnstream = false;
UafilmME.apn = null;
}
RegisterWithSearch("uafilmme");
}
private static void RegisterWithSearch(string plugin)
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
{
foreach (var item in list)
{
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
return;
}
list.Add(plugin);
}
}
catch
{
}
}
public void Dispose()
{
}
}
public static class UpdateService
{
private static readonly string _connectUrl = "https://lmcuk.lme.isroot.in/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<ConnectResponse>(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
{
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);
}

View File

@ -0,0 +1,67 @@
using System.Collections.Generic;
namespace UafilmME.Models
{
public class UafilmSearchItem
{
public long Id { get; set; }
public string Name { get; set; }
public string OriginalTitle { get; set; }
public bool IsSeries { get; set; }
public int Year { get; set; }
public string ImdbId { get; set; }
public long TmdbId { get; set; }
public string Poster { get; set; }
public int MatchScore { get; set; }
}
public class UafilmTitleDetails
{
public long Id { get; set; }
public string Name { get; set; }
public string OriginalTitle { get; set; }
public bool IsSeries { get; set; }
public int Year { get; set; }
public string ImdbId { get; set; }
public long TmdbId { get; set; }
public int SeasonsCount { get; set; }
public long PrimaryVideoId { get; set; }
}
public class UafilmSeasonItem
{
public long Id { get; set; }
public int Number { get; set; }
public int EpisodesCount { get; set; }
}
public class UafilmEpisodeItem
{
public long Id { get; set; }
public string Name { get; set; }
public int SeasonNumber { get; set; }
public int EpisodeNumber { get; set; }
public long PrimaryVideoId { get; set; }
public string PrimaryVideoName { get; set; }
}
public class UafilmVideoItem
{
public long Id { get; set; }
public string Name { get; set; }
public string Src { get; set; }
public string Type { get; set; }
public string Quality { get; set; }
public string Origin { get; set; }
public string Language { get; set; }
public int? SeasonNum { get; set; }
public int? EpisodeNum { get; set; }
public long EpisodeId { get; set; }
}
public class UafilmWatchInfo
{
public UafilmVideoItem Video { get; set; }
public List<UafilmVideoItem> AlternativeVideos { get; set; } = new();
}
}

33
UafilmME/OnlineApi.cs Normal file
View File

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Http;
using Shared.Models;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
namespace UafilmME
{
public class OnlineApi : IModuleOnline
{
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, 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);
}
private static List<ModuleOnlineItem> 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<ModuleOnlineItem>();
var init = ModInit.UafilmME;
if (init.enable && !init.rip)
{
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add(new ModuleOnlineItem(init, "uafilmme"));
}
return online;
}
}
}

15
UafilmME/UafilmME.csproj Normal file
View File

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

751
UafilmME/UafilmMEInvoke.cs Normal file
View File

@ -0,0 +1,751 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.Settings;
using UafilmME.Models;
namespace UafilmME
{
public class UafilmMEInvoke
{
private readonly OnlinesSettings _init;
private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
private readonly HttpHydra _httpHydra;
public UafilmMEInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_httpHydra = httpHydra;
}
public async Task<List<UafilmSearchItem>> Search(string title, string originalTitle, int year)
{
var queries = BuildSearchQueries(title, originalTitle, year).ToList();
if (queries.Count == 0)
return new List<UafilmSearchItem>();
var all = new Dictionary<long, UafilmSearchItem>();
foreach (var query in queries)
{
var items = await SearchByQuery(query);
foreach (var item in items)
all[item.Id] = item;
}
return all.Values.ToList();
}
public UafilmSearchItem SelectBestSearchResult(List<UafilmSearchItem> results, long tmdbId, string imdbId, string title, string originalTitle, int year, int serial)
{
if (results == null || results.Count == 0)
return null;
foreach (var item in results)
item.MatchScore = CalcMatchScore(item, tmdbId, imdbId, title, originalTitle, year, serial);
return results
.OrderByDescending(r => r.MatchScore)
.ThenByDescending(r => r.Year)
.FirstOrDefault();
}
public async Task<UafilmTitleDetails> GetTitleDetails(long titleId)
{
string memKey = $"UafilmME:title:{titleId}";
if (_hybridCache.TryGetValue(memKey, out UafilmTitleDetails cached))
return cached;
try
{
string json = await ApiGet($"titles/{titleId}?loader=titlePage", $"{_init.host}/titles/{titleId}");
var title = ParseTitleDetails(json);
if (title != null)
_hybridCache.Set(memKey, title, cacheTime(30, init: _init));
return title;
}
catch (Exception ex)
{
_onLog?.Invoke($"UafilmME: помилка отримання title {titleId}: {ex.Message}");
return null;
}
}
public async Task<List<UafilmSeasonItem>> GetAllSeasons(long titleId)
{
string memKey = $"UafilmME:seasons:{titleId}";
if (_hybridCache.TryGetValue(memKey, out List<UafilmSeasonItem> cached))
return cached;
var all = new List<UafilmSeasonItem>();
int currentPage = 1;
int guard = 0;
while (currentPage > 0 && guard < 100)
{
guard++;
var page = await GetSeasonsPage(titleId, currentPage);
if (page.Items.Count == 0)
break;
all.AddRange(page.Items);
if (page.NextPage.HasValue && page.NextPage.Value != currentPage)
currentPage = page.NextPage.Value;
else
break;
}
var result = all
.GroupBy(s => s.Number)
.Select(g => g.OrderByDescending(x => x.EpisodesCount).First())
.OrderBy(s => s.Number)
.ToList();
if (result.Count == 0)
{
var title = await GetTitleDetails(titleId);
if (title?.SeasonsCount > 0)
{
for (int i = 1; i <= title.SeasonsCount; i++)
{
result.Add(new UafilmSeasonItem()
{
Number = i,
EpisodesCount = 0
});
}
}
}
if (result.Count > 0)
_hybridCache.Set(memKey, result, cacheTime(60, init: _init));
return result;
}
public async Task<List<UafilmEpisodeItem>> GetSeasonEpisodes(long titleId, int season)
{
string memKey = $"UafilmME:episodes:{titleId}:{season}";
if (_hybridCache.TryGetValue(memKey, out List<UafilmEpisodeItem> cached))
return cached;
var all = new List<UafilmEpisodeItem>();
int currentPage = 1;
int guard = 0;
while (currentPage > 0 && guard < 200)
{
guard++;
var page = await GetEpisodesPage(titleId, season, currentPage);
if (page.Items.Count == 0)
break;
all.AddRange(page.Items);
if (page.NextPage.HasValue && page.NextPage.Value != currentPage)
currentPage = page.NextPage.Value;
else
break;
}
var result = all
.GroupBy(e => e.Id)
.Select(g => g.First())
.OrderBy(e => e.EpisodeNumber)
.ToList();
if (result.Count > 0)
_hybridCache.Set(memKey, result, cacheTime(30, init: _init));
return result;
}
public async Task<List<UafilmVideoItem>> GetMovieVideos(long titleId)
{
var title = await GetTitleDetails(titleId);
if (title == null || title.PrimaryVideoId <= 0)
return new List<UafilmVideoItem>();
var watch = await GetWatch(title.PrimaryVideoId);
return CollectPlayableVideos(watch);
}
public async Task<UafilmWatchInfo> GetWatch(long videoId)
{
if (videoId <= 0)
return null;
string memKey = $"UafilmME:watch:{videoId}";
if (_hybridCache.TryGetValue(memKey, out UafilmWatchInfo cached))
return cached;
try
{
string json = await ApiGet($"watch/{videoId}", _init.host);
var watch = ParseWatchInfo(json);
if (watch?.Video != null)
_hybridCache.Set(memKey, watch, cacheTime(7, init: _init));
return watch;
}
catch (Exception ex)
{
_onLog?.Invoke($"UafilmME: помилка отримання watch/{videoId}: {ex.Message}");
return null;
}
}
public List<UafilmVideoItem> CollectPlayableVideos(UafilmWatchInfo watch)
{
var list = new List<UafilmVideoItem>();
if (watch == null)
return list;
if (watch.Video != null)
list.Add(watch.Video);
if (watch.AlternativeVideos != null && watch.AlternativeVideos.Count > 0)
list.AddRange(watch.AlternativeVideos);
return list
.Where(v => v != null && v.Id > 0)
.Select(v =>
{
v.Src = NormalizeVideoSource(v.Src);
return v;
})
.Where(v => !string.IsNullOrWhiteSpace(v.Src))
.Where(v => !string.Equals(v.Type, "embed", StringComparison.OrdinalIgnoreCase))
.Where(v => v.Src.IndexOf("youtube.com", StringComparison.OrdinalIgnoreCase) < 0)
.GroupBy(v => v.Id)
.Select(g => g.First())
.ToList();
}
private async Task<List<UafilmSearchItem>> SearchByQuery(string query)
{
string memKey = $"UafilmME:search:{query}";
if (_hybridCache.TryGetValue(memKey, out List<UafilmSearchItem> cached))
return cached;
string encoded = HttpUtility.UrlEncode(query);
string json = await ApiGet($"search/{encoded}?loader=searchPage", $"{_init.host}/search/{encoded}");
var items = ParseSearchResults(json);
if (items.Count > 0)
_hybridCache.Set(memKey, items, cacheTime(20, init: _init));
return items;
}
private async Task<(List<UafilmSeasonItem> Items, int? NextPage)> GetSeasonsPage(long titleId, int page)
{
string memKey = $"UafilmME:seasons-page:{titleId}:{page}";
if (_hybridCache.TryGetValue(memKey, out List<UafilmSeasonItem> cachedItems) &&
_hybridCache.TryGetValue(memKey + ":next", out int? cachedNext))
{
return (cachedItems, cachedNext);
}
string suffix = page > 1 ? $"?page={page}" : string.Empty;
string json = await ApiGet($"titles/{titleId}/seasons{suffix}", $"{_init.host}/titles/{titleId}");
var parsed = ParseSeasonsPage(json);
_hybridCache.Set(memKey, parsed.Items, cacheTime(30, init: _init));
_hybridCache.Set(memKey + ":next", parsed.NextPage, cacheTime(30, init: _init));
return parsed;
}
private async Task<(List<UafilmEpisodeItem> Items, int? NextPage)> GetEpisodesPage(long titleId, int season, int page)
{
string memKey = $"UafilmME:episodes-page:{titleId}:{season}:{page}";
if (_hybridCache.TryGetValue(memKey, out List<UafilmEpisodeItem> cachedItems) &&
_hybridCache.TryGetValue(memKey + ":next", out int? cachedNext))
{
return (cachedItems, cachedNext);
}
string suffix = page > 1 ? $"?page={page}" : string.Empty;
string json = await ApiGet($"titles/{titleId}/seasons/{season}/episodes{suffix}", $"{_init.host}/titles/{titleId}");
var parsed = ParseEpisodesPage(json);
_hybridCache.Set(memKey, parsed.Items, cacheTime(20, init: _init));
_hybridCache.Set(memKey + ":next", parsed.NextPage, cacheTime(20, init: _init));
return parsed;
}
private async Task<string> ApiGet(string pathAndQuery, string referer)
{
string url = $"{_init.host.TrimEnd('/')}/api/v1/{pathAndQuery.TrimStart('/')}";
string reqReferer = string.IsNullOrWhiteSpace(referer) ? $"{_init.host}/" : referer;
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "EchoapiRuntime/1.1.0"),
new HeadersModel("Referer", reqReferer),
new HeadersModel("Accept", "*/*")
};
if (_httpHydra != null)
return await _httpHydra.Get(url, newheaders: headers);
return await Http.Get(url, headers: headers, proxy: _proxyManager.Get());
}
private string NormalizeVideoSource(string src)
{
if (string.IsNullOrWhiteSpace(src))
return null;
src = src.Trim();
if (src.StartsWith("//"))
return "https:" + src;
if (src.StartsWith("/"))
return _init.host.TrimEnd('/') + src;
return src;
}
private static IEnumerable<string> BuildSearchQueries(string title, string originalTitle, int year)
{
var queries = new List<string>();
void Add(string value)
{
if (!string.IsNullOrWhiteSpace(value))
queries.Add(value.Trim());
}
Add(title);
Add(originalTitle);
if (year > 1900)
{
if (!string.IsNullOrWhiteSpace(title))
Add($"{title} {year}");
if (!string.IsNullOrWhiteSpace(originalTitle))
Add($"{originalTitle} {year}");
}
return queries
.Where(q => !string.IsNullOrWhiteSpace(q))
.Distinct(StringComparer.OrdinalIgnoreCase);
}
private List<UafilmSearchItem> ParseSearchResults(string json)
{
var list = new List<UafilmSearchItem>();
if (string.IsNullOrWhiteSpace(json))
return list;
using var doc = JsonDocument.Parse(json);
if (!TryGetArray(doc.RootElement, "results", out var results))
return list;
foreach (var item in results.EnumerateArray())
{
if (!TryReadLong(item, "id", out long id) || id <= 0)
continue;
list.Add(new UafilmSearchItem()
{
Id = id,
Name = ReadString(item, "name"),
OriginalTitle = ReadString(item, "original_title"),
IsSeries = ReadBool(item, "is_series"),
Year = ReadInt(item, "year"),
ImdbId = ReadString(item, "imdb_id"),
TmdbId = ReadLong(item, "tmdb_id"),
Poster = ReadString(item, "poster")
});
}
return list;
}
private UafilmTitleDetails ParseTitleDetails(string json)
{
if (string.IsNullOrWhiteSpace(json))
return null;
using var doc = JsonDocument.Parse(json);
if (!TryGetObject(doc.RootElement, "title", out var titleObj))
return null;
var info = new UafilmTitleDetails()
{
Id = ReadLong(titleObj, "id"),
Name = ReadString(titleObj, "name"),
OriginalTitle = ReadString(titleObj, "original_title"),
IsSeries = ReadBool(titleObj, "is_series"),
Year = ReadInt(titleObj, "year"),
ImdbId = ReadString(titleObj, "imdb_id"),
TmdbId = ReadLong(titleObj, "tmdb_id"),
SeasonsCount = ReadInt(titleObj, "seasons_count")
};
if (TryGetObject(titleObj, "primary_video", out var primaryVideo))
info.PrimaryVideoId = ReadLong(primaryVideo, "id");
return info;
}
private (List<UafilmSeasonItem> Items, int? NextPage) ParseSeasonsPage(string json)
{
var items = new List<UafilmSeasonItem>();
int? next = null;
if (string.IsNullOrWhiteSpace(json))
return (items, next);
using var doc = JsonDocument.Parse(json);
if (!TryGetObject(doc.RootElement, "pagination", out var pagination))
return (items, next);
next = ReadNullableInt(pagination, "next_page");
if (!TryGetArray(pagination, "data", out var data))
return (items, next);
foreach (var item in data.EnumerateArray())
{
int number = ReadInt(item, "number");
if (number <= 0)
continue;
items.Add(new UafilmSeasonItem()
{
Id = ReadLong(item, "id"),
Number = number,
EpisodesCount = ReadInt(item, "episodes_count")
});
}
return (items, next);
}
private (List<UafilmEpisodeItem> Items, int? NextPage) ParseEpisodesPage(string json)
{
var items = new List<UafilmEpisodeItem>();
int? next = null;
if (string.IsNullOrWhiteSpace(json))
return (items, next);
using var doc = JsonDocument.Parse(json);
if (!TryGetObject(doc.RootElement, "pagination", out var pagination))
return (items, next);
next = ReadNullableInt(pagination, "next_page");
if (!TryGetArray(pagination, "data", out var data))
return (items, next);
foreach (var item in data.EnumerateArray())
{
long episodeId = ReadLong(item, "id");
if (episodeId <= 0)
continue;
long primaryVideoId = 0;
string primaryVideoName = null;
if (TryGetObject(item, "primary_video", out var primaryVideoObj))
{
primaryVideoId = ReadLong(primaryVideoObj, "id");
primaryVideoName = ReadString(primaryVideoObj, "name");
}
items.Add(new UafilmEpisodeItem()
{
Id = episodeId,
Name = ReadString(item, "name"),
SeasonNumber = ReadInt(item, "season_number"),
EpisodeNumber = ReadInt(item, "episode_number"),
PrimaryVideoId = primaryVideoId,
PrimaryVideoName = primaryVideoName
});
}
return (items, next);
}
private UafilmWatchInfo ParseWatchInfo(string json)
{
if (string.IsNullOrWhiteSpace(json))
return null;
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.ValueKind != JsonValueKind.Object)
return null;
var watch = new UafilmWatchInfo();
if (TryGetObject(doc.RootElement, "video", out var videoObj))
watch.Video = ParseVideo(videoObj);
if (TryGetArray(doc.RootElement, "alternative_videos", out var alternatives))
{
foreach (var alt in alternatives.EnumerateArray())
{
var parsed = ParseVideo(alt);
if (parsed != null)
watch.AlternativeVideos.Add(parsed);
}
}
return watch;
}
private static UafilmVideoItem ParseVideo(JsonElement obj)
{
long id = ReadLong(obj, "id");
if (id <= 0)
return null;
return new UafilmVideoItem()
{
Id = id,
Name = ReadString(obj, "name"),
Src = ReadString(obj, "src"),
Type = ReadString(obj, "type"),
Quality = ReadString(obj, "quality"),
Origin = ReadString(obj, "origin"),
Language = ReadString(obj, "language"),
SeasonNum = ReadNullableInt(obj, "season_num"),
EpisodeNum = ReadNullableInt(obj, "episode_num"),
EpisodeId = ReadLong(obj, "episode_id")
};
}
private int CalcMatchScore(UafilmSearchItem item, long tmdbId, string imdbId, string title, string originalTitle, int year, int serial)
{
int score = 0;
if (item == null)
return score;
if (tmdbId > 0 && item.TmdbId == tmdbId)
score += 120;
if (!string.IsNullOrWhiteSpace(imdbId) && !string.IsNullOrWhiteSpace(item.ImdbId) && string.Equals(item.ImdbId.Trim(), imdbId.Trim(), StringComparison.OrdinalIgnoreCase))
score += 120;
if (serial == 1)
score += item.IsSeries ? 25 : -25;
else
score += item.IsSeries ? -15 : 15;
if (year > 1900 && item.Year > 1900)
{
int diff = Math.Abs(item.Year - year);
if (diff == 0)
score += 20;
else if (diff == 1)
score += 10;
else if (diff == 2)
score += 5;
else
score -= 6;
}
score += ScoreTitle(item.Name, title);
score += ScoreTitle(item.Name, originalTitle);
score += ScoreTitle(item.OriginalTitle, title);
score += ScoreTitle(item.OriginalTitle, originalTitle);
return score;
}
private static int ScoreTitle(string candidate, string expected)
{
if (string.IsNullOrWhiteSpace(candidate) || string.IsNullOrWhiteSpace(expected))
return 0;
string left = NormalizeTitle(candidate);
string right = NormalizeTitle(expected);
if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right))
return 0;
if (left == right)
return 35;
if (left.Contains(right) || right.Contains(left))
return 20;
var leftWords = left.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var rightWords = right.Split(' ', StringSplitOptions.RemoveEmptyEntries);
int overlap = leftWords.Intersect(rightWords).Count();
if (overlap >= 2)
return 12;
if (overlap == 1)
return 6;
return 0;
}
private static string NormalizeTitle(string value)
{
if (string.IsNullOrWhiteSpace(value))
return string.Empty;
string normalized = value.ToLowerInvariant();
normalized = Regex.Replace(normalized, "[^\\p{L}\\p{Nd}]+", " ", RegexOptions.CultureInvariant);
normalized = Regex.Replace(normalized, "\\s+", " ", RegexOptions.CultureInvariant).Trim();
return normalized;
}
private static bool TryGetObject(JsonElement source, string property, out JsonElement value)
{
value = default;
if (!source.TryGetProperty(property, out var prop) || prop.ValueKind != JsonValueKind.Object)
return false;
value = prop;
return true;
}
private static bool TryGetArray(JsonElement source, string property, out JsonElement value)
{
value = default;
if (!source.TryGetProperty(property, out var prop) || prop.ValueKind != JsonValueKind.Array)
return false;
value = prop;
return true;
}
private static string ReadString(JsonElement source, string property)
{
if (!source.TryGetProperty(property, out var value))
return null;
if (value.ValueKind == JsonValueKind.String)
return value.GetString();
if (value.ValueKind == JsonValueKind.Number)
return value.GetRawText();
if (value.ValueKind == JsonValueKind.True)
return bool.TrueString;
if (value.ValueKind == JsonValueKind.False)
return bool.FalseString;
return null;
}
private static bool ReadBool(JsonElement source, string property)
{
if (!source.TryGetProperty(property, out var value))
return false;
if (value.ValueKind == JsonValueKind.True)
return true;
if (value.ValueKind == JsonValueKind.False)
return false;
if (value.ValueKind == JsonValueKind.Number)
return value.GetInt32() != 0;
if (value.ValueKind == JsonValueKind.String)
{
string text = value.GetString();
if (bool.TryParse(text, out bool parsedBool))
return parsedBool;
if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedInt))
return parsedInt != 0;
}
return false;
}
private static int ReadInt(JsonElement source, string property)
{
if (!source.TryGetProperty(property, out var value))
return 0;
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int number))
return number;
if (value.ValueKind == JsonValueKind.String && int.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
return parsed;
return 0;
}
private static int? ReadNullableInt(JsonElement source, string property)
{
if (!source.TryGetProperty(property, out var value))
return null;
if (value.ValueKind == JsonValueKind.Null)
return null;
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int number))
return number;
if (value.ValueKind == JsonValueKind.String && int.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
return parsed;
return null;
}
private static long ReadLong(JsonElement source, string property)
{
return TryReadLong(source, property, out long value)
? value
: 0;
}
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 = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;
return TimeSpan.FromMinutes(ctime);
}
private static bool TryReadLong(JsonElement source, string property, out long value)
{
value = 0;
if (!source.TryGetProperty(property, out var element))
return false;
if (element.ValueKind == JsonValueKind.Number && element.TryGetInt64(out long number))
{
value = number;
return true;
}
if (element.ValueKind == JsonValueKind.String && long.TryParse(element.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out long parsed))
{
value = parsed;
return true;
}
return false;
}
}
}

6
UafilmME/manifest.json Normal file
View File

@ -0,0 +1,6 @@
{
"enable": true,
"version": 3,
"initspace": "UafilmME.ModInit",
"online": "UafilmME.OnlineApi"
}

View File

@ -25,6 +25,23 @@ namespace Shared.Engine
return true;
}
public static string TryGetMagicAshdiHost(JObject conf)
{
if (conf == null || !conf.TryGetValue("magic_apn", out var magicToken) || magicToken == null)
return null;
if (magicToken.Type == JTokenType.Boolean)
return magicToken.Value<bool>() ? DefaultHost : null;
if (magicToken.Type == JTokenType.String)
return NormalizeHost(magicToken.Value<string>());
if (magicToken.Type != JTokenType.Object)
return null;
return NormalizeHost(((JObject)magicToken).Value<string>("ashdi"));
}
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
{
if (init == null)
@ -37,8 +54,13 @@ namespace Shared.Engine
return;
}
if (string.IsNullOrWhiteSpace(host))
host = DefaultHost;
host = NormalizeHost(host);
if (host == null)
{
init.apnstream = false;
init.apn = null;
return;
}
if (init.apn == null)
init.apn = new ApnConf();
@ -82,5 +104,13 @@ namespace Shared.Engine
return $"{host.TrimEnd('/')}/{url}";
}
private static string NormalizeHost(string host)
{
if (string.IsNullOrWhiteSpace(host))
return null;
return host.Trim();
}
}
}

View File

@ -28,28 +28,29 @@ namespace Uaflix.Controllers
}
[HttpGet]
[Route("uaflix")]
[Route("lite/uaflix")]
async public Task<ActionResult> 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 t, int s = -1, int e = -1, bool play = false, bool rjson = false, string href = null, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.UaFlix);
if (await IsBadInitialization(init))
return Forbid();
if (await IsRequestBlocked(rch: false))
return badInitMsg;
var init = this.init;
TryEnableMagicApn(init);
OnLog($"=== UAFLIX INDEX START ===");
OnLog($"Uaflix Index: title={title}, serial={serial}, s={s}, play={play}, href={href}, checksearch={checksearch}");
OnLog($"Uaflix Index: kinopoisk_id={kinopoisk_id}, imdb_id={imdb_id}, id={id}");
OnLog($"Uaflix Index: year={year}, source={source}, t={t}, e={e}, rjson={rjson}");
var auth = new UaflixAuth(init, memoryCache, OnLog, proxyManager);
var invoke = new UaflixInvoke(init, hybridCache, OnLog, proxyManager, auth);
var invoke = new UaflixInvoke(init, hybridCache, OnLog, proxyManager, auth, httpHydra);
// Обробка параметра checksearch - повертаємо спеціальну відповідь для валідації
if (checksearch)
{
if (AppInit.conf?.online?.checkOnlineSearch != true)
return OnError("uaflix", proxyManager);
if (!IsCheckOnlineSearchEnabled())
return OnError("uaflix", refresh_proxy: true);
try
{
@ -63,13 +64,13 @@ namespace Uaflix.Controllers
OnLog("checksearch: Контент не знайдено");
OnLog("=== RETURN: checksearch OnError ===");
return OnError("uaflix", proxyManager);
return OnError("uaflix", refresh_proxy: true);
}
catch (Exception ex)
{
OnLog($"checksearch: помилка - {ex.Message}");
OnLog("=== RETURN: checksearch exception OnError ===");
return OnError("uaflix", proxyManager);
return OnError("uaflix", refresh_proxy: true);
}
}
@ -80,7 +81,7 @@ namespace Uaflix.Controllers
if (string.IsNullOrWhiteSpace(urlToParse))
{
OnLog("=== RETURN: play missing url OnError ===");
return OnError("uaflix", proxyManager);
return OnError("uaflix", refresh_proxy: true);
}
var playResult = await invoke.ParseEpisode(urlToParse);
@ -91,7 +92,7 @@ namespace Uaflix.Controllers
}
OnLog("=== RETURN: play no streams ===");
return OnError("uaflix", proxyManager);
return OnError("uaflix", refresh_proxy: true);
}
// Якщо є episode_url але немає play=true, це виклик для отримання інформації про стрім (для method: 'call')
@ -109,7 +110,7 @@ namespace Uaflix.Controllers
}
OnLog("=== RETURN: call method no streams ===");
return OnError("uaflix", proxyManager);
return OnError("uaflix", refresh_proxy: true);
}
string filmUrl = href;
@ -121,7 +122,7 @@ namespace Uaflix.Controllers
{
OnLog("No search results found");
OnLog("=== RETURN: no search results OnError ===");
return OnError("uaflix", proxyManager);
return OnError("uaflix", refresh_proxy: true);
}
var selectedResult = invoke.SelectBestSearchResult(searchResults, title, original_title, year);
@ -142,7 +143,7 @@ namespace Uaflix.Controllers
var similar_tpl = new SimilarTpl(orderedResults.Count);
foreach (var res in orderedResults)
{
string link = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}";
string link = $"{host}/lite/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}";
string y = res.Year > 0 ? res.Year.ToString() : string.Empty;
string details = res.Category switch
{
@ -162,97 +163,50 @@ namespace Uaflix.Controllers
if (serial == 1)
{
// Агрегуємо всі озвучки з усіх плеєрів
var structure = await invoke.AggregateSerialStructure(filmUrl);
if (structure == null || !structure.Voices.Any())
{
OnLog("No voices found in aggregated structure");
OnLog("=== RETURN: no voices OnError ===");
return OnError("uaflix", proxyManager);
}
OnLog($"Structure aggregated successfully: {structure.Voices.Count} voices, URL: {filmUrl}");
foreach (var voice in structure.Voices)
{
OnLog($"Voice: {voice.Key}, Type: {voice.Value.PlayerType}, Seasons: {voice.Value.Seasons.Count}");
foreach (var season in voice.Value.Seasons)
{
OnLog($" Season {season.Key}: {season.Value.Count} episodes");
}
}
// s == -1: Вибір сезону
// s == -1: швидкий вибір сезону без повної агрегації серіалу
if (s == -1)
{
List<int> allSeasons;
VoiceInfo tVoice = null;
bool restrictByVoice = !string.IsNullOrEmpty(t) && structure.Voices.TryGetValue(t, out tVoice) && IsAshdiVoice(tVoice);
if (restrictByVoice)
var seasonIndex = await invoke.GetSeasonIndex(filmUrl);
var seasons = seasonIndex?.Seasons?.Keys
.Distinct()
.OrderBy(sn => sn)
.ToList();
if (seasons == null || seasons.Count == 0)
{
allSeasons = GetSeasonSet(tVoice).OrderBy(sn => sn).ToList();
OnLog($"Ashdi voice selected (t='{t}'), seasons count={allSeasons.Count}");
}
else
{
allSeasons = structure.Voices
.SelectMany(v => GetSeasonSet(v.Value))
.Distinct()
.OrderBy(sn => sn)
.ToList();
OnLog("No seasons found in season index");
OnLog("=== RETURN: no seasons OnError ===");
return OnError("uaflix", refresh_proxy: true);
}
OnLog($"Found {allSeasons.Count} seasons in structure: {string.Join(", ", allSeasons)}");
// Перевіряємо чи сезони містять валідні епізоди з файлами
var seasonsWithValidEpisodes = allSeasons.Where(season =>
structure.Voices.Values.Any(v =>
v.Seasons.ContainsKey(season) &&
v.Seasons[season].Any(ep => !string.IsNullOrEmpty(ep.File))
)
).ToList();
OnLog($"Seasons with valid episodes: {seasonsWithValidEpisodes.Count}");
foreach (var season in allSeasons)
var season_tpl = new SeasonTpl(seasons.Count);
foreach (int season in seasons)
{
var episodesInSeason = structure.Voices.Values
.Where(v => v.Seasons.ContainsKey(season))
.SelectMany(v => v.Seasons[season])
.Where(ep => !string.IsNullOrEmpty(ep.File))
.ToList();
OnLog($"Season {season}: {episodesInSeason.Count} valid episodes");
}
if (!seasonsWithValidEpisodes.Any())
{
OnLog("No seasons with valid episodes found in structure");
OnLog("=== RETURN: no valid seasons OnError ===");
return OnError("uaflix", proxyManager);
}
var season_tpl = new SeasonTpl(seasonsWithValidEpisodes.Count);
foreach (var season in seasonsWithValidEpisodes)
{
string link = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season}&href={HttpUtility.UrlEncode(filmUrl)}";
if (restrictByVoice)
string link = $"{host}/lite/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season}&href={HttpUtility.UrlEncode(filmUrl)}";
if (!string.IsNullOrWhiteSpace(t))
link += $"&t={HttpUtility.UrlEncode(t)}";
season_tpl.Append($"{season}", link, season.ToString());
OnLog($"Added season {season} to template");
}
OnLog($"Returning season template with {seasonsWithValidEpisodes.Count} seasons");
var htmlContent = rjson ? season_tpl.ToJson() : season_tpl.ToHtml();
OnLog($"Season template response length: {htmlContent.Length}");
OnLog($"Season template HTML (first 300): {htmlContent.Substring(0, Math.Min(300, htmlContent.Length))}");
OnLog($"=== RETURN: season template ({seasonsWithValidEpisodes.Count} seasons) ===");
return Content(htmlContent, rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8");
OnLog($"=== RETURN: season template ({seasons.Count} seasons) ===");
return Content(
rjson ? season_tpl.ToJson() : season_tpl.ToHtml(),
rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8"
);
}
// s >= 0: Показуємо озвучки + епізоди
else if (s >= 0)
// s >= 0: завантажуємо тільки потрібний сезон
if (s >= 0)
{
var structure = await invoke.GetSeasonStructure(filmUrl, s);
if (structure == null || structure.Voices == null || structure.Voices.Count == 0)
{
OnLog($"No voices found for season {s}");
OnLog("=== RETURN: no voices for season OnError ===");
return OnError("uaflix", refresh_proxy: true);
}
var voicesForSeason = structure.Voices
.Where(v => v.Value.Seasons.ContainsKey(s))
.Select(v => new { DisplayName = v.Key, Info = v.Value })
.ToList();
@ -260,7 +214,7 @@ namespace Uaflix.Controllers
{
OnLog($"No voices found for season {s}");
OnLog("=== RETURN: no voices for season OnError ===");
return OnError("uaflix", proxyManager);
return OnError("uaflix", refresh_proxy: true);
}
// Автоматично вибираємо першу озвучку якщо не вказана
@ -275,68 +229,56 @@ namespace Uaflix.Controllers
OnLog($"Voice '{t}' not found, fallback to first voice: {t}");
}
VoiceInfo selectedVoice = null;
if (!structure.Voices.TryGetValue(t, out selectedVoice) || !selectedVoice.Seasons.ContainsKey(s) || selectedVoice.Seasons[s] == null || selectedVoice.Seasons[s].Count == 0)
{
var fallbackVoice = voicesForSeason.FirstOrDefault(v => v.Info.Seasons.ContainsKey(s) && v.Info.Seasons[s] != null && v.Info.Seasons[s].Count > 0);
if (fallbackVoice == null)
{
OnLog($"Season {s} not found for selected voice and fallback voice missing");
OnLog("=== RETURN: season not found for voice OnError ===");
return OnError("uaflix", refresh_proxy: true);
}
t = fallbackVoice.DisplayName;
selectedVoice = fallbackVoice.Info;
OnLog($"Selected voice had no episodes, fallback to: {t}");
}
// Створюємо VoiceTpl з усіма озвучками
var voice_tpl = new VoiceTpl();
var selectedVoiceInfo = structure.Voices[t];
var selectedSeasonSet = GetSeasonSet(selectedVoiceInfo);
bool selectedIsAshdi = IsAshdiVoice(selectedVoiceInfo);
foreach (var voice in voicesForSeason)
{
bool targetIsAshdi = IsAshdiVoice(voice.Info);
var targetSeasonSet = GetSeasonSet(voice.Info);
bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet);
bool needSeasonReset = (selectedIsAshdi || targetIsAshdi) && !sameSeasonSet;
string voiceLink = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&href={HttpUtility.UrlEncode(filmUrl)}";
if (needSeasonReset)
voiceLink += $"&s=-1&t={HttpUtility.UrlEncode(voice.DisplayName)}";
else
voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.DisplayName)}";
string voiceLink = $"{host}/lite/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&href={HttpUtility.UrlEncode(filmUrl)}";
voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.DisplayName)}";
bool isActive = voice.DisplayName == t;
voice_tpl.Append(voice.DisplayName, isActive, voiceLink);
}
OnLog($"Created VoiceTpl with {voicesForSeason.Count} voices, active: {t}");
// Відображення епізодів для вибраної озвучки
if (!structure.Voices.ContainsKey(t))
{
OnLog($"Voice '{t}' not found in structure");
OnLog("=== RETURN: voice not found OnError ===");
return OnError("uaflix", proxyManager);
}
if (!structure.Voices[t].Seasons.ContainsKey(s))
{
OnLog($"Season {s} not found for voice '{t}'");
if (IsAshdiVoice(structure.Voices[t]))
{
string redirectUrl = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s=-1&t={HttpUtility.UrlEncode(t)}&href={HttpUtility.UrlEncode(filmUrl)}";
OnLog($"Ashdi voice missing season, redirect to season selector: {redirectUrl}");
return Redirect(redirectUrl);
}
OnLog("=== RETURN: season not found for voice OnError ===");
return OnError("uaflix", proxyManager);
}
var episodes = structure.Voices[t].Seasons[s];
var episodes = selectedVoice.Seasons[s];
var episode_tpl = new EpisodeTpl();
int appendedEpisodes = 0;
foreach (var ep in episodes)
{
if (ep == null || string.IsNullOrWhiteSpace(ep.File))
continue;
string episodeTitle = !string.IsNullOrWhiteSpace(ep.Title) ? ep.Title : $"Епізод {ep.Number}";
// Для zetvideo-vod повертаємо URL епізоду з методом call
// Для ashdi/zetvideo-serial повертаємо готове посилання з play
var voice = structure.Voices[t];
var voice = selectedVoice;
if (voice.PlayerType == "zetvideo-vod" || voice.PlayerType == "ashdi-vod")
{
// Для zetvideo-vod та ashdi-vod використовуємо URL епізоду для виклику
// Потрібно передати URL епізоду в інший параметр, щоб не плутати з play=true
string callUrl = $"{host}/uaflix?episode_url={HttpUtility.UrlEncode(ep.File)}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&s={s}&e={ep.Number}";
string callUrl = $"{host}/lite/uaflix?episode_url={HttpUtility.UrlEncode(ep.File)}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&s={s}&e={ep.Number}";
episode_tpl.Append(
name: ep.Title,
name: episodeTitle,
title: title,
s: s.ToString(),
e: ep.Number.ToString(),
@ -350,16 +292,25 @@ namespace Uaflix.Controllers
// Для багатосерійних плеєрів (ashdi-serial, zetvideo-serial) - пряме відтворення
string playUrl = BuildStreamUrl(init, ep.File);
episode_tpl.Append(
name: ep.Title,
name: episodeTitle,
title: title,
s: s.ToString(),
e: ep.Number.ToString(),
link: playUrl
);
}
appendedEpisodes++;
}
OnLog($"Created EpisodeTpl with {episodes.Count} episodes");
if (appendedEpisodes == 0)
{
OnLog($"No valid episodes after filtering for season {s}, voice {t}");
OnLog("=== RETURN: no valid episodes OnError ===");
return OnError("uaflix", refresh_proxy: true);
}
OnLog($"Created EpisodeTpl with {appendedEpisodes} episodes");
// Повертаємо VoiceTpl + EpisodeTpl разом
episode_tpl.Append(voice_tpl);
@ -378,7 +329,7 @@ namespace Uaflix.Controllers
// Fallback: якщо жоден з умов не виконався
OnLog($"Fallback: s={s}, t={t}");
OnLog("=== RETURN: fallback OnError ===");
return OnError("uaflix", proxyManager);
return OnError("uaflix", refresh_proxy: true);
}
else // Фільм
{
@ -386,7 +337,7 @@ namespace Uaflix.Controllers
if (playResult?.streams == null || playResult.streams.Count == 0)
{
OnLog("=== RETURN: movie no streams ===");
return OnError("uaflix", proxyManager);
return OnError("uaflix", refresh_proxy: true);
}
var tpl = new MovieTpl(title, original_title, playResult.streams.Count);
@ -407,7 +358,7 @@ namespace Uaflix.Controllers
if (tpl.data == null || tpl.data.Count == 0)
{
OnLog("=== RETURN: movie template empty ===");
return OnError("uaflix", proxyManager);
return OnError("uaflix", refresh_proxy: true);
}
OnLog("=== RETURN: movie template ===");
@ -435,6 +386,24 @@ namespace Uaflix.Controllers
return HostStreamProxy(init, link);
}
private void TryEnableMagicApn(OnlinesSettings init)
{
if (init == null
|| init.apn != null
|| init.streamproxy
|| string.IsNullOrWhiteSpace(ModInit.MagicApnAshdiHost))
return;
string player = new RchClient(HttpContext, host, init, requestInfo).InfoConnected()?.player;
bool useInnerPlayer = string.IsNullOrWhiteSpace(player)
|| player.Equals("inner", StringComparison.OrdinalIgnoreCase);
if (!useInnerPlayer)
return;
ApnHelper.ApplyInitConf(true, ModInit.MagicApnAshdiHost, init);
OnLog($"Uaflix: увімкнено magic_apn для Ashdi (player={player ?? "unknown"}).");
}
private static string StripLampacArgs(string url)
{
if (string.IsNullOrEmpty(url))
@ -451,23 +420,37 @@ namespace Uaflix.Controllers
return cleaned;
}
private static bool IsAshdiVoice(VoiceInfo voice)
private static bool IsCheckOnlineSearchEnabled()
{
if (voice == null || string.IsNullOrEmpty(voice.PlayerType))
return false;
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
return voice.PlayerType == "ashdi-serial" || voice.PlayerType == "ashdi-vod";
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static HashSet<int> GetSeasonSet(VoiceInfo voice)
private static void OnLog(string message)
{
if (voice?.Seasons == null || voice.Seasons.Count == 0)
return new HashSet<int>();
return voice.Seasons
.Where(kv => kv.Value != null && kv.Value.Any(ep => !string.IsNullOrEmpty(ep.File)))
.Select(kv => kv.Key)
.ToHashSet();
System.Console.WriteLine(message);
}
}
}

4
Uaflix/GlobalUsings.cs Normal file
View File

@ -0,0 +1,4 @@
global using Shared.Services;
global using Shared.Services.Hybrid;
global using Shared.Models.Base;
global using AppInit = Shared.CoreInit;

View File

@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
using Shared;
using Shared.Engine;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System;
using System.Net.Http;
using System.Net.Mime;
@ -16,13 +17,14 @@ using Uaflix.Models;
namespace Uaflix
{
public class ModInit
public class ModInit : IModuleLoaded
{
public static double Version => 4.0;
public static double Version => 5.1;
public static UaflixSettings UaFlix;
public static bool ApnHostProvided;
public static string MagicApnAshdiHost;
public static UaflixSettings Settings
{
@ -33,7 +35,7 @@ namespace Uaflix
/// <summary>
/// Модуль завантажено.
/// </summary>
public static void loaded(InitspaceModel initspace)
public void Loaded(InitspaceModel initspace)
{
UaFlix = new UaflixSettings("Uaflix", "https://uafix.net", streamproxy: false, useproxy: false)
{
@ -53,8 +55,16 @@ namespace Uaflix
}
};
var conf = ModuleInvoke.Conf("Uaflix", UaFlix) ?? JObject.FromObject(UaFlix);
var defaults = JObject.FromObject(UaFlix);
defaults["magic_apn"] = new JObject()
{
["ashdi"] = ApnHelper.DefaultHost
};
var conf = ModuleInvoke.Init("Uaflix", defaults) ?? defaults;
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
MagicApnAshdiHost = ApnHelper.TryGetMagicAshdiHost(conf);
conf.Remove("magic_apn");
conf.Remove("apn");
conf.Remove("apn_host");
UaFlix = conf.ToObject<UaflixSettings>();
@ -62,8 +72,8 @@ namespace Uaflix
if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, UaFlix);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
if (hasApn && apnEnabled)
ApnHostProvided = ApnHelper.IsEnabled(UaFlix);
if (ApnHostProvided)
{
UaFlix.streamproxy = false;
}
@ -74,7 +84,45 @@ namespace Uaflix
}
// Показувати «уточнити пошук».
AppInit.conf.online.with_search.Add("uaflix");
RegisterWithSearch("uaflix");
}
private static void RegisterWithSearch(string plugin)
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
{
foreach (var item in list)
{
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
return;
}
list.Add(plugin);
}
}
catch
{
}
}
public void Dispose()
{
}
}

View File

@ -7,6 +7,9 @@ namespace Uaflix.Models
{
// Словник сезонів, де ключ - номер сезону, значення - кількість сторінок
public Dictionary<int, int> Seasons { get; set; } = new Dictionary<int, int>();
// URL сторінки сезону: ключ - номер сезону, значення - абсолютний URL сторінки
public Dictionary<int, string> SeasonUrls { get; set; } = new Dictionary<int, string>();
// Загальна кількість сторінок (якщо потрібно)
public int TotalPages { get; set; }
@ -16,4 +19,4 @@ namespace Uaflix.Models
public List<EpisodeLinkInfo> Episodes { get; set; } = new List<EpisodeLinkInfo>();
}
}
}

View File

@ -1,37 +1,32 @@
using Microsoft.AspNetCore.Http;
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 Uaflix
{
public class OnlineApi
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Uaflix
{
public class OnlineApi : IModuleOnline
{
public static List<(string name, string url, string plugin, int index)> Invoke(
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, 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)
private static List<ModuleOnlineItem> 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 online = new List<ModuleOnlineItem>();
var init = ModInit.UaFlix;
if (init.enable && !init.rip)
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
url = $"{host}/uaflix";
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add((init.displayname, url, "uaflix", init.displayindex));
online.Add(new ModuleOnlineItem(init, "uaflix"));
}
return online;

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>

View File

@ -28,14 +28,16 @@ namespace Uaflix
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
private readonly UaflixAuth _auth;
private readonly HttpHydra _httpHydra;
public UaflixInvoke(UaflixSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, UaflixAuth auth)
public UaflixInvoke(UaflixSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, UaflixAuth auth, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_auth = auth;
_httpHydra = httpHydra;
}
string AshdiRequestUrl(string url)
@ -76,7 +78,6 @@ namespace Uaflix
if (string.IsNullOrWhiteSpace(url))
return null;
string requestUrl = _init.cors(url);
bool withAuth = ShouldUseAuth(url);
var requestHeaders = headers != null ? new List<HeadersModel>(headers) : new List<HeadersModel>();
@ -86,6 +87,26 @@ namespace Uaflix
_auth.ApplyCookieHeader(requestHeaders, cookie);
}
if (_httpHydra != null)
{
string content = await _httpHydra.Get(url, newheaders: requestHeaders, statusCodeOK: false);
if (string.IsNullOrWhiteSpace(content)
&& retryOnUnauthorized
&& withAuth
&& _auth != null
&& _auth.CanUseCredentials)
{
_onLog($"UaflixAuth: порожня відповідь для {url}, виконую повторну авторизацію");
string refreshedCookie = await _auth.GetCookieHeaderAsync(forceRefresh: true);
_auth.ApplyCookieHeader(requestHeaders, refreshedCookie);
content = await _httpHydra.Get(url, newheaders: requestHeaders, statusCodeOK: false);
}
return string.IsNullOrWhiteSpace(content) ? null : content;
}
string requestUrl = _init.cors(url);
var response = await Http.BaseGet(requestUrl,
headers: requestHeaders,
timeoutSeconds: timeoutSeconds,
@ -800,6 +821,440 @@ namespace Uaflix
#endregion
#region Сезонний (лінивий) парсинг серіалу
public async Task<PaginationInfo> GetSeasonIndex(string serialUrl)
{
string memKey = $"UaFlix:season-index:{serialUrl}";
if (_hybridCache.TryGetValue(memKey, out PaginationInfo cached))
return cached;
try
{
if (string.IsNullOrWhiteSpace(serialUrl) || !Uri.IsWellFormedUriString(serialUrl, UriKind.Absolute))
return null;
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", _init.host)
};
string html = await GetHtml(serialUrl, headers);
if (string.IsNullOrWhiteSpace(html))
return null;
var doc = new HtmlDocument();
doc.LoadHtml(html);
var result = new PaginationInfo
{
SerialUrl = serialUrl
};
var seasonNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'sez-wr')]//a");
if (seasonNodes == null)
seasonNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'fss-box')]//a");
if (seasonNodes == null || seasonNodes.Count == 0)
{
// Якщо явного списку сезонів немає, вважаємо що є один сезон.
result.Seasons[1] = 1;
result.SeasonUrls[1] = serialUrl;
_hybridCache.Set(memKey, result, cacheTime(40));
return result;
}
foreach (var node in seasonNodes)
{
string href = node.GetAttributeValue("href", null);
string seasonUrl = ToAbsoluteUrl(href);
if (string.IsNullOrWhiteSpace(seasonUrl))
continue;
string tabText = WebUtility.HtmlDecode(node.InnerText ?? string.Empty);
if (!IsSeasonTabLink(seasonUrl, tabText))
continue;
int season = ExtractSeasonNumber(seasonUrl, tabText);
if (season <= 0)
continue;
if (!result.SeasonUrls.TryGetValue(season, out string existing))
{
result.SeasonUrls[season] = seasonUrl;
result.Seasons[season] = 1;
continue;
}
if (IsPreferableSeasonUrl(existing, seasonUrl, season))
result.SeasonUrls[season] = seasonUrl;
}
if (result.SeasonUrls.Count == 0)
{
result.Seasons[1] = 1;
result.SeasonUrls[1] = serialUrl;
}
_hybridCache.Set(memKey, result, cacheTime(40));
return result;
}
catch (Exception ex)
{
_onLog($"GetSeasonIndex error: {ex.Message}");
return null;
}
}
public async Task<List<EpisodeLinkInfo>> GetSeasonEpisodes(string serialUrl, int season)
{
if (season < 0)
return new List<EpisodeLinkInfo>();
string memKey = $"UaFlix:season-episodes:{serialUrl}:{season}";
if (_hybridCache.TryGetValue(memKey, out List<EpisodeLinkInfo> cached))
return cached;
try
{
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", _init.host)
};
var index = await GetSeasonIndex(serialUrl);
string seasonUrl = index?.SeasonUrls != null && index.SeasonUrls.TryGetValue(season, out string mapped)
? mapped
: serialUrl;
if (string.IsNullOrWhiteSpace(seasonUrl))
seasonUrl = serialUrl;
string html = await GetHtml(seasonUrl, headers);
if (string.IsNullOrWhiteSpace(html) && !string.Equals(seasonUrl, serialUrl, StringComparison.OrdinalIgnoreCase))
html = await GetHtml(serialUrl, headers);
if (string.IsNullOrWhiteSpace(html))
return new List<EpisodeLinkInfo>();
var result = ParseSeasonEpisodesFromHtml(html, season);
if (result.Count == 0 && !string.Equals(seasonUrl, serialUrl, StringComparison.OrdinalIgnoreCase))
{
string serialHtml = await GetHtml(serialUrl, headers);
if (!string.IsNullOrWhiteSpace(serialHtml))
result = ParseSeasonEpisodesFromHtml(serialHtml, season);
}
if (result.Count == 0 && season == 1 && string.Equals(seasonUrl, serialUrl, StringComparison.OrdinalIgnoreCase))
{
// Fallback для сторінок без окремих епізодів.
result.Add(new EpisodeLinkInfo
{
url = serialUrl,
title = "Епізод 1",
season = 1,
episode = 1
});
}
_hybridCache.Set(memKey, result, cacheTime(20));
return result;
}
catch (Exception ex)
{
_onLog($"GetSeasonEpisodes error: {ex.Message}");
return new List<EpisodeLinkInfo>();
}
}
List<EpisodeLinkInfo> ParseSeasonEpisodesFromHtml(string html, int season)
{
if (string.IsNullOrWhiteSpace(html))
return new List<EpisodeLinkInfo>();
var doc = new HtmlDocument();
doc.LoadHtml(html);
var episodeNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'frels')]//a[contains(@class, 'vi-img')]");
if (episodeNodes == null || episodeNodes.Count == 0)
return new List<EpisodeLinkInfo>();
var episodes = new List<EpisodeLinkInfo>();
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
int fallbackEpisode = 1;
foreach (var episodeNode in episodeNodes)
{
string episodeUrl = ToAbsoluteUrl(episodeNode.GetAttributeValue("href", null));
if (string.IsNullOrWhiteSpace(episodeUrl) || !used.Add(episodeUrl))
continue;
var match = Regex.Match(episodeUrl, @"season-(\d+).*?episode-(\d+)", RegexOptions.IgnoreCase);
int parsedSeason = season;
int parsedEpisode = fallbackEpisode;
if (match.Success)
{
if (int.TryParse(match.Groups[1].Value, out int seasonFromUrl))
parsedSeason = seasonFromUrl;
if (int.TryParse(match.Groups[2].Value, out int episodeFromUrl))
parsedEpisode = episodeFromUrl;
}
episodes.Add(new EpisodeLinkInfo
{
url = episodeUrl,
title = episodeNode.SelectSingleNode(".//div[@class='vi-rate']")?.InnerText.Trim() ?? $"Епізод {parsedEpisode}",
season = parsedSeason,
episode = parsedEpisode
});
fallbackEpisode = Math.Max(fallbackEpisode, parsedEpisode + 1);
}
return episodes
.Where(e => e != null && !string.IsNullOrWhiteSpace(e.url))
.Where(e => e.season == season)
.OrderBy(e => e.episode)
.ToList();
}
public async Task<SerialAggregatedStructure> GetSeasonStructure(string serialUrl, int season)
{
if (season < 0)
return null;
string memKey = $"UaFlix:season-structure:{serialUrl}:{season}";
if (_hybridCache.TryGetValue(memKey, out SerialAggregatedStructure cached))
{
_onLog($"GetSeasonStructure: Using cached structure for season={season}, url={serialUrl}");
return cached;
}
try
{
var seasonEpisodes = await GetSeasonEpisodes(serialUrl, season);
if (seasonEpisodes == null || seasonEpisodes.Count == 0)
{
_onLog($"GetSeasonStructure: No episodes for season={season}, url={serialUrl}");
return null;
}
var structure = new SerialAggregatedStructure
{
SerialUrl = serialUrl,
AllEpisodes = seasonEpisodes
};
var seasonProbe = await ProbeSeasonPlayer(seasonEpisodes);
if (string.IsNullOrWhiteSpace(seasonProbe.playerType))
{
// fallback: інколи плеєр є лише на головній сторінці
seasonProbe = await ProbeEpisodePlayer(serialUrl);
if (string.IsNullOrWhiteSpace(seasonProbe.playerType))
{
_onLog($"GetSeasonStructure: unsupported player for season={season}");
return null;
}
}
if (seasonProbe.playerType == "ashdi-serial" || seasonProbe.playerType == "zetvideo-serial")
{
var voices = await ParseMultiEpisodePlayerCached(seasonProbe.iframeUrl, seasonProbe.playerType);
foreach (var voice in voices)
{
if (voice?.Seasons == null || !voice.Seasons.TryGetValue(season, out List<EpisodeInfo> seasonVoiceEpisodes) || seasonVoiceEpisodes == null || seasonVoiceEpisodes.Count == 0)
continue;
structure.Voices[voice.DisplayName] = new VoiceInfo
{
Name = voice.Name,
PlayerType = voice.PlayerType,
DisplayName = voice.DisplayName,
Seasons = new Dictionary<int, List<EpisodeInfo>>
{
[season] = seasonVoiceEpisodes
.Where(ep => ep != null && !string.IsNullOrWhiteSpace(ep.File))
.Select(ep => new EpisodeInfo
{
Number = ep.Number,
Title = ep.Title,
File = ep.File,
Id = ep.Id,
Poster = ep.Poster,
Subtitle = ep.Subtitle
})
.ToList()
}
};
}
}
else if (seasonProbe.playerType == "ashdi-vod" || seasonProbe.playerType == "zetvideo-vod")
{
AddVodSeasonEpisodes(structure, seasonProbe.playerType, season, seasonEpisodes);
}
else
{
_onLog($"GetSeasonStructure: player '{seasonProbe.playerType}' is not supported");
return null;
}
if (!structure.Voices.Any())
{
_onLog($"GetSeasonStructure: voices are empty for season={season}, url={serialUrl}");
return null;
}
NormalizeUaflixVoiceNames(structure);
_hybridCache.Set(memKey, structure, cacheTime(30));
return structure;
}
catch (Exception ex)
{
_onLog($"GetSeasonStructure error: {ex.Message}");
return null;
}
}
async Task<List<VoiceInfo>> ParseMultiEpisodePlayerCached(string iframeUrl, string playerType)
{
string serialKey = NormalizeSerialPlayerKey(playerType, iframeUrl);
string memKey = $"UaFlix:player-voices:{playerType}:{serialKey}";
if (_hybridCache.TryGetValue(memKey, out List<VoiceInfo> cached))
return CloneVoices(cached);
var parsed = await ParseMultiEpisodePlayer(iframeUrl, playerType);
if (parsed == null || parsed.Count == 0)
return new List<VoiceInfo>();
_hybridCache.Set(memKey, parsed, cacheTime(40));
return CloneVoices(parsed);
}
static List<VoiceInfo> CloneVoices(List<VoiceInfo> voices)
{
if (voices == null || voices.Count == 0)
return new List<VoiceInfo>();
var result = new List<VoiceInfo>(voices.Count);
foreach (var voice in voices)
{
if (voice == null)
continue;
var clone = new VoiceInfo
{
Name = voice.Name,
PlayerType = voice.PlayerType,
DisplayName = voice.DisplayName,
Seasons = new Dictionary<int, List<EpisodeInfo>>()
};
if (voice.Seasons != null)
{
foreach (var season in voice.Seasons)
{
clone.Seasons[season.Key] = season.Value?
.Where(ep => ep != null)
.Select(ep => new EpisodeInfo
{
Number = ep.Number,
Title = ep.Title,
File = ep.File,
Id = ep.Id,
Poster = ep.Poster,
Subtitle = ep.Subtitle
})
.ToList() ?? new List<EpisodeInfo>();
}
}
result.Add(clone);
}
return result;
}
string ToAbsoluteUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
return null;
string clean = WebUtility.HtmlDecode(url.Trim());
if (clean.StartsWith("//"))
clean = "https:" + clean;
if (clean.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || clean.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
return clean;
if (string.IsNullOrWhiteSpace(_init?.host))
return clean;
return $"{_init.host.TrimEnd('/')}/{clean.TrimStart('/')}";
}
static bool IsSeasonTabLink(string url, string text)
{
string u = (url ?? string.Empty).ToLowerInvariant();
string t = (text ?? string.Empty).ToLowerInvariant();
if (u.Contains("/date/") || t.Contains("графік") || t.Contains("дата виходу"))
return false;
if (Regex.IsMatch(u, @"(?:sezon|season)[-_/ ]?\d+", RegexOptions.IgnoreCase))
return true;
if (Regex.IsMatch(t, @"(?:сезон|season)\s*\d+", RegexOptions.IgnoreCase))
return true;
return false;
}
static bool IsPreferableSeasonUrl(string oldUrl, string newUrl, int season)
{
if (string.IsNullOrWhiteSpace(newUrl))
return false;
if (string.IsNullOrWhiteSpace(oldUrl))
return true;
string marker = $"/sezon-{season}/";
bool oldHasMarker = oldUrl.IndexOf(marker, StringComparison.OrdinalIgnoreCase) >= 0;
bool newHasMarker = newUrl.IndexOf(marker, StringComparison.OrdinalIgnoreCase) >= 0;
if (!oldHasMarker && newHasMarker)
return true;
return false;
}
static int ExtractSeasonNumber(string url, string text)
{
foreach (string source in new[] { url, text })
{
if (string.IsNullOrWhiteSpace(source))
continue;
var seasonBySlug = Regex.Match(source, @"(?:sezon|season)[-_/ ]?(\d+)", RegexOptions.IgnoreCase);
if (seasonBySlug.Success && int.TryParse(seasonBySlug.Groups[1].Value, out int seasonSlug) && seasonSlug > 0)
return seasonSlug;
var seasonByWordUa = Regex.Match(source, @"сезон\s*(\d+)", RegexOptions.IgnoreCase);
if (seasonByWordUa.Success && int.TryParse(seasonByWordUa.Groups[1].Value, out int seasonWordUa) && seasonWordUa > 0)
return seasonWordUa;
var seasonByWordEn = Regex.Match(source, @"season\s*(\d+)", RegexOptions.IgnoreCase);
if (seasonByWordEn.Success && int.TryParse(seasonByWordEn.Groups[1].Value, out int seasonWordEn) && seasonWordEn > 0)
return seasonWordEn;
}
return 0;
}
#endregion
public async Task<List<SearchResult>> Search(string imdb_id, long kinopoisk_id, string title, string original_title, int year, int serial, string original_language, string source, string search_query)
{
bool allowAnime = IsAnimeRequest(title, original_title, original_language, source);
@ -1790,7 +2245,7 @@ namespace Uaflix
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;
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;
@ -1805,9 +2260,9 @@ namespace Uaflix
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 (init != null && ctime > init.cache_time && init.cache_time > 0)
ctime = init.cache_time;
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;
return TimeSpan.FromMinutes(ctime);
}

View File

@ -1,6 +1,3 @@
{
"enable": true,
"version": 3,
"initspace": "Uaflix.ModInit",
"online": "Uaflix.OnlineApi"
"enable": true
}

View File

@ -1,4 +1,3 @@
using Shared.Engine;
using System;
using System.Threading.Tasks;
using System.Linq;
@ -22,20 +21,20 @@ namespace Unimay.Controllers
}
[HttpGet]
[Route("unimay")]
[Route("lite/unimay")]
async public ValueTask<ActionResult> Index(string title, string original_title, string code, int serial = -1, int s = -1, int e = -1, bool play = false, bool rjson = false, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Unimay);
if (await IsBadInitialization(init, rch: false))
if (await IsRequestBlocked(rch: false))
return badInitMsg;
var invoke = new UnimayInvoke(init, hybridCache, OnLog, proxyManager);
var init = this.init;
var invoke = new UnimayInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch)
{
if (AppInit.conf?.online?.checkOnlineSearch != true)
if (!IsCheckOnlineSearchEnabled())
return OnError("unimay");
var searchResults = await invoke.Search(title, original_title, serial);
@ -169,5 +168,38 @@ namespace Unimay.Controllers
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned;
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

4
Unimay/GlobalUsings.cs Normal file
View File

@ -0,0 +1,4 @@
global using Shared.Services;
global using Shared.Services.Hybrid;
global using Shared.Models.Base;
global using AppInit = Shared.CoreInit;

View File

@ -1,20 +1,10 @@
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Newtonsoft.Json.Linq;
using Shared;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Newtonsoft.Json.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Caching.Memory;
using Shared.Models;
using Shared.Models.Events;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Shared;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Shared.Models.Online.Settings;
using System;
using System.Net.Http;
using System.Net.Mime;
@ -28,9 +18,9 @@ using System.Threading.Tasks;
namespace Unimay
{
public class ModInit
public class ModInit : IModuleLoaded
{
public static double Version => 3.4;
public static double Version => 4.0;
public static OnlinesSettings Unimay;
@ -43,7 +33,7 @@ namespace Unimay
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
public void Loaded(InitspaceModel initspace)
{
@ -59,10 +49,48 @@ namespace Unimay
list = new string[] { "socks5://IP:PORT" }
}
};
Unimay = ModuleInvoke.Conf("Unimay", Unimay).ToObject<OnlinesSettings>();
Unimay = ModuleInvoke.Init("Unimay", JObject.FromObject(Unimay)).ToObject<OnlinesSettings>();
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("unimay");
RegisterWithSearch("unimay");
}
private static void RegisterWithSearch(string plugin)
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
{
foreach (var item in list)
{
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
return;
}
list.Add(plugin);
}
}
catch
{
}
}
public void Dispose()
{
}
}
@ -184,4 +212,4 @@ namespace Unimay
}
public record ConnectResponse(bool IsUpdateUnavailable, bool IsNoiseEnabled);
}
}

View File

@ -1,47 +1,36 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Unimay
{
public class OnlineApi
public class OnlineApi : IModuleOnline
{
public static List<(string name, string url, string plugin, int index)> Invoke(
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, 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)
private static List<ModuleOnlineItem> 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 online = new List<ModuleOnlineItem>();
var init = ModInit.Unimay;
// Визначення isAnime згідно стандарту Lampac (Deepwiki):
// isanime = true якщо original_language == "ja" або "zh"
bool hasLang = !string.IsNullOrEmpty(original_language);
bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
// Unimay — аніме-провайдер. Додаємо якщо:
// - загальний пошук (serial == -1), або
// - контент є аніме (isanime), або
// - мова невідома (немає original_language)
if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang))
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
url = $"{host}/unimay";
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add((init.displayname, url, "unimay", init.displayindex));
online.Add(new ModuleOnlineItem(init, "unimay"));
}
return online;

View File

@ -1,11 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<LangVersion>latest</LangVersion>
<TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
@ -14,4 +12,4 @@
</Reference>
</ItemGroup>
</Project>
</Project>

View File

@ -6,7 +6,6 @@ using Shared.Models.Online.Settings;
using Shared.Models;
using System.Linq;
using Unimay.Models;
using Shared.Engine;
using System.Net;
using System.Text;
@ -18,13 +17,15 @@ namespace Unimay
private ProxyManager _proxyManager;
private IHybridCache _hybridCache;
private Action<string> _onLog;
private readonly HttpHydra _httpHydra;
public UnimayInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
public UnimayInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_httpHydra = httpHydra;
}
public async Task<SearchResponse> Search(string title, string original_title, int serial)
@ -39,7 +40,7 @@ namespace Unimay
string searchUrl = $"{_init.host}/release/search?page=0&page_size=10&title={searchQuery}";
var headers = httpHeaders(_init);
SearchResponse root = await Http.Get<SearchResponse>(_init.cors(searchUrl), timeoutSeconds: 8, proxy: _proxyManager.Get(), headers: headers);
SearchResponse root = await HttpGet<SearchResponse>(searchUrl, headers, timeoutSeconds: 8);
if (root == null || root.Content == null || root.Content.Count == 0)
{
@ -69,7 +70,7 @@ namespace Unimay
string releaseUrl = $"{_init.host}/release?code={code}";
var headers = httpHeaders(_init);
ReleaseResponse root = await Http.Get<ReleaseResponse>(_init.cors(releaseUrl), timeoutSeconds: 8, proxy: _proxyManager.Get(), headers: headers);
ReleaseResponse root = await HttpGet<ReleaseResponse>(releaseUrl, headers, timeoutSeconds: 8);
if (root == null)
{
@ -103,7 +104,7 @@ namespace Unimay
}
string itemTitle = item.Names?.Ukr ?? item.Names?.Eng ?? item.Title;
string releaseUrl = $"{host}/unimay?code={item.Code}&title={System.Web.HttpUtility.UrlEncode(itemTitle)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial={serial}";
string releaseUrl = $"{host}/lite/unimay?code={item.Code}&title={System.Web.HttpUtility.UrlEncode(itemTitle)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial={serial}";
results.Add((itemTitle, item.Year, item.Type, releaseUrl));
}
@ -116,7 +117,7 @@ namespace Unimay
return (null, null);
var movieEpisode = releaseDetail.Playlist[0];
string movieLink = $"{host}/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=0&play=true";
string movieLink = $"{host}/lite/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=0&play=true";
string movieTitle = movieEpisode.Title ?? title;
return (movieTitle, movieLink);
@ -124,7 +125,7 @@ namespace Unimay
public (string seasonName, string seasonUrl, string seasonId) GetSeasonInfo(string host, string code, string title, string original_title)
{
string seasonUrl = $"{host}/unimay?code={code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1";
string seasonUrl = $"{host}/lite/unimay?code={code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1";
return ("Сезон 1", seasonUrl, "1");
}
@ -138,7 +139,7 @@ namespace Unimay
foreach (var ep in releaseDetail.Playlist.Where(ep => ep.Number >= 1 && ep.Number <= 24).OrderBy(ep => ep.Number))
{
string epTitle = ep.Title ?? $"Епізод {ep.Number}";
string epLink = $"{host}/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1&e={ep.Number}&play=true";
string epLink = $"{host}/lite/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1&e={ep.Number}&play=true";
episodes.Add((epTitle, epLink));
}
@ -160,12 +161,20 @@ namespace Unimay
};
}
private Task<T> HttpGet<T>(string url, List<HeadersModel> headers, int timeoutSeconds = 15)
{
if (_httpHydra != null)
return _httpHydra.Get<T>(url, newheaders: headers);
return Http.Get<T>(_init.cors(url), timeoutSeconds: timeoutSeconds, proxy: _proxyManager.Get(), headers: headers);
}
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;
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;

View File

@ -1,6 +1,3 @@
{
"enable": true,
"version": 3,
"initspace": "Unimay.ModInit",
"online": "Unimay.OnlineApi"
"enable": true
}