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/ /.qodo/
.DS_Store .DS_Store
AGENTS.md AGENTS.md
/planing/ /planing/
.vs
bin
obj
.vscode/settings.json

View File

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

View File

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

View File

@ -25,6 +25,23 @@ namespace Shared.Engine
return true; 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) public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
{ {
if (init == null) if (init == null)
@ -37,8 +54,13 @@ namespace Shared.Engine
return; return;
} }
if (string.IsNullOrWhiteSpace(host)) host = NormalizeHost(host);
host = DefaultHost; if (host == null)
{
init.apnstream = false;
init.apn = null;
return;
}
if (init.apn == null) if (init.apn == null)
init.apn = new ApnConf(); init.apn = new ApnConf();
@ -82,5 +104,13 @@ namespace Shared.Engine
return $"{host.TrimEnd('/')}/{url}"; 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] [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) 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); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.AnimeON); var init = loadKit(ModInit.AnimeON);
if (!init.enable) if (!init.enable)
return Forbid(); return Forbid();
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager); TryEnableMagicApn(init);
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch) if (checksearch)
{ {
if (AppInit.conf?.online?.checkOnlineSearch != true) if (!IsCheckOnlineSearchEnabled())
return OnError("animeon", proxyManager); return OnError("animeon", refresh_proxy: true);
var checkSeasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial); var checkSeasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial);
if (checkSeasons != null && checkSeasons.Count > 0) if (checkSeasons != null && checkSeasons.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8"); 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}"); 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); var seasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial);
OnLog($"AnimeON: search results = {seasons?.Count ?? 0}"); OnLog($"AnimeON: search results = {seasons?.Count ?? 0}");
if (seasons == null || seasons.Count == 0) if (seasons == null || seasons.Count == 0)
return OnError("animeon", proxyManager); return OnError("animeon", refresh_proxy: true);
// [Refactoring] Використовується агрегована структура (AggregateSerialStructure) — попередній збір allOptions не потрібний // [Refactoring] Використовується агрегована структура (AggregateSerialStructure) — попередній збір allOptions не потрібний
@ -81,7 +82,7 @@ namespace AnimeON.Controllers
foreach (var item in seasonItems) foreach (var item in seasonItems)
{ {
string seasonName = item.SeasonNumber.ToString(); 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); season_tpl.Append(seasonName, link, seasonName);
} }
OnLog($"AnimeON: return seasons count={seasonItems.Count}"); 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 }; selected = new { Anime = seasons[s], Index = s, SeasonNumber = seasons[s].Season > 0 ? seasons[s].Season : s + 1 };
if (selected == null) if (selected == null)
return OnError("animeon", proxyManager); return OnError("animeon", refresh_proxy: true);
var selectedAnime = selected.Anime; var selectedAnime = selected.Anime;
int selectedSeasonNumber = selected.SeasonNumber; int selectedSeasonNumber = selected.SeasonNumber;
var structure = await invoke.AggregateSerialStructure(selectedAnime.Id, selectedSeasonNumber); var structure = await invoke.AggregateSerialStructure(selectedAnime.Id, selectedSeasonNumber);
if (structure == null || !structure.Voices.Any()) if (structure == null || !structure.Voices.Any())
return OnError("animeon", proxyManager); return OnError("animeon", refresh_proxy: true);
OnLog($"AnimeON: voices found = {structure.Voices.Count}"); OnLog($"AnimeON: voices found = {structure.Voices.Count}");
var voiceItems = structure.Voices var voiceItems = structure.Voices
@ -135,14 +136,14 @@ namespace AnimeON.Controllers
var voice_tpl = new VoiceTpl(); var voice_tpl = new VoiceTpl();
foreach (var voice in voiceItems) 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; bool isActive = voice.Key == t;
voice_tpl.Append(voice.Display, isActive, voiceLink); voice_tpl.Append(voice.Display, isActive, voiceLink);
} }
// Перевірка вибраної озвучки // Перевірка вибраної озвучки
if (!structure.Voices.ContainsKey(t)) if (!structure.Voices.ContainsKey(t))
return OnError("animeon", proxyManager); return OnError("animeon", refresh_proxy: true);
var episode_tpl = new EpisodeTpl(); var episode_tpl = new EpisodeTpl();
var selectedVoiceInfo = structure.Voices[t]; var selectedVoiceInfo = structure.Voices[t];
@ -179,7 +180,7 @@ namespace AnimeON.Controllers
if (string.IsNullOrEmpty(streamLink) && ep.EpisodeId > 0) 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"); episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
continue; continue;
} }
@ -189,7 +190,7 @@ namespace AnimeON.Controllers
if (needsResolve || streamLink.Contains("moonanime.art") || streamLink.Contains("ashdi.vip/vod")) 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"); episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
} }
else else
@ -212,12 +213,12 @@ namespace AnimeON.Controllers
{ {
var firstAnime = seasons.FirstOrDefault(); var firstAnime = seasons.FirstOrDefault();
if (firstAnime == null) if (firstAnime == null)
return OnError("animeon", proxyManager); return OnError("animeon", refresh_proxy: true);
var fundubs = await invoke.GetFundubs(firstAnime.Id); var fundubs = await invoke.GetFundubs(firstAnime.Id);
OnLog($"AnimeON: movie fundubs count = {fundubs?.Count ?? 0}"); OnLog($"AnimeON: movie fundubs count = {fundubs?.Count ?? 0}");
if (fundubs == null || 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); var tpl = new MovieTpl(title, original_title);
@ -251,7 +252,7 @@ namespace AnimeON.Controllers
foreach (var ashdiStream in ashdiStreams) foreach (var ashdiStream in ashdiStreams)
{ {
string optionName = $"{translationName} {ashdiStream.title}"; 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"); tpl.Append(optionName, accsArgs(callUrl), "call");
} }
continue; continue;
@ -260,7 +261,7 @@ namespace AnimeON.Controllers
if (needsResolve || streamLink.Contains("moonanime.art/iframe/") || streamLink.Contains("ashdi.vip/vod")) 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"); tpl.Append(translationName, accsArgs(callUrl), "call");
} }
else else
@ -272,7 +273,7 @@ namespace AnimeON.Controllers
// Якщо не зібрали жодної опції — повертаємо помилку // Якщо не зібрали жодної опції — повертаємо помилку
if (tpl.data == null || tpl.data.Count == 0) if (tpl.data == null || tpl.data.Count == 0)
return OnError("animeon", proxyManager); return OnError("animeon", refresh_proxy: true);
OnLog("AnimeON: return movie options"); OnLog("AnimeON: return movie options");
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8"); 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 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)) if (string.IsNullOrEmpty(fundubsJson))
return null; 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 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)) if (string.IsNullOrEmpty(episodesJson))
return null; return null;
@ -332,7 +333,7 @@ namespace AnimeON.Controllers
string searchUrl = $"{init.host}/api/anime/search?text={HttpUtility.UrlEncode(query)}"; 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)) if (string.IsNullOrEmpty(searchJson))
return null; return null;
@ -373,16 +374,17 @@ namespace AnimeON.Controllers
return null; 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) public async Task<ActionResult> Play(string url, int episode_id = 0, string title = null, int serial = 0)
{ {
await UpdateService.ConnectAsync(host); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.AnimeON); var init = loadKit(ModInit.AnimeON);
if (!init.enable) if (!init.enable)
return Forbid(); 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; bool disableAshdiMultivoiceForVod = serial == 1;
OnLog($"AnimeON Play: url={url}, episode_id={episode_id}, serial={serial}"); OnLog($"AnimeON Play: url={url}, episode_id={episode_id}, serial={serial}");
@ -398,13 +400,13 @@ namespace AnimeON.Controllers
else else
{ {
OnLog("AnimeON Play: empty url"); OnLog("AnimeON Play: empty url");
return OnError("animeon", proxyManager); return OnError("animeon", refresh_proxy: true);
} }
if (string.IsNullOrEmpty(streamLink)) if (string.IsNullOrEmpty(streamLink))
{ {
OnLog("AnimeON Play: cannot extract stream"); OnLog("AnimeON Play: cannot extract stream");
return OnError("animeon", proxyManager); return OnError("animeon", refresh_proxy: true);
} }
List<HeadersModel> streamHeaders = null; List<HeadersModel> streamHeaders = null;
@ -462,5 +464,56 @@ namespace AnimeON.Controllers
return HostStreamProxy(init, link, headers: headers, force_streamproxy: forceProxy); 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;
using Shared.Engine; using Shared.Engine;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting; using Microsoft.CodeAnalysis.Scripting;
@ -23,12 +24,13 @@ using System.Threading.Tasks;
namespace AnimeON 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 OnlinesSettings AnimeON;
public static bool ApnHostProvided; public static bool ApnHostProvided;
public static string MagicApnAshdiHost;
public static OnlinesSettings Settings public static OnlinesSettings Settings
{ {
@ -39,7 +41,7 @@ namespace AnimeON
/// <summary> /// <summary>
/// модуль загружен /// модуль загружен
/// </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" } 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); 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");
conf.Remove("apn_host"); conf.Remove("apn_host");
AnimeON = conf.ToObject<OnlinesSettings>(); AnimeON = conf.ToObject<OnlinesSettings>();
if (hasApn) if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, AnimeON); ApnHelper.ApplyInitConf(apnEnabled, apnHost, AnimeON);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost); ApnHostProvided = ApnHelper.IsEnabled(AnimeON);
if (hasApn && apnEnabled) if (ApnHostProvided)
{ {
AnimeON.streamproxy = false; 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); 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 Microsoft.Extensions.Caching.Memory;
using Shared.Models; using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module; using Shared.Models.Module;
using System.Collections.Generic; using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AnimeON namespace AnimeON
{ {
public class OnlineApi public class OnlineApi : IModuleOnline
{ {
public static List<(string name, string url, string plugin, int index)> Invoke( public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
{ {
long.TryParse(args.id, out long tmdbid); 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); 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; var init = ModInit.AnimeON;
// Визначаємо isAnime згідно стандарту Lampac (Deepwiki):
// isanime = true якщо original_language == "ja" або "zh"
bool hasLang = !string.IsNullOrEmpty(original_language); bool hasLang = !string.IsNullOrEmpty(original_language);
bool isanime = hasLang && (original_language == "ja" || original_language == "zh"); bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
// AnimeON — аніме-провайдер. Додаємо його:
// - при загальному пошуку (serial == -1), або
// - якщо контент визначений як аніме (isanime), або
// - якщо мова невідома (відсутній original_language)
if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang)) if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang))
{ {
string url = init.overridehost; if (UpdateService.IsDisconnected())
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) init.overridehost = null;
url = $"{host}/animeon";
online.Add((init.displayname, url, "animeon", init.displayindex)); online.Add(new ModuleOnlineItem(init, "animeon"));
} }
return online; return online;

View File

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

View File

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

View File

@ -20,13 +20,15 @@ namespace Bamboo
private readonly IHybridCache _hybridCache; private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog; private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager; 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; _init = init;
_hybridCache = hybridCache; _hybridCache = hybridCache;
_onLog = onLog; _onLog = onLog;
_proxyManager = proxyManager; _proxyManager = proxyManager;
_httpHydra = httpHydra;
} }
public async Task<List<SearchResult>> Search(string title, string original_title) public async Task<List<SearchResult>> Search(string title, string original_title)
@ -50,7 +52,7 @@ namespace Bamboo
}; };
_onLog?.Invoke($"Bamboo search: {searchUrl}"); _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)) if (string.IsNullOrEmpty(html))
return null; return null;
@ -109,7 +111,7 @@ namespace Bamboo
}; };
_onLog?.Invoke($"Bamboo series page: {href}"); _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)) if (string.IsNullOrEmpty(html))
return null; return null;
@ -183,7 +185,7 @@ namespace Bamboo
}; };
_onLog?.Invoke($"Bamboo movie page: {href}"); _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)) if (string.IsNullOrEmpty(html))
return null; return null;
@ -311,12 +313,20 @@ namespace Bamboo
return HtmlEntity.DeEntitize(value).Trim(); 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) 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) if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub); 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) if (ctime > multiaccess)
ctime = multiaccess; ctime = multiaccess;

View File

@ -23,27 +23,27 @@ namespace Bamboo.Controllers
} }
[HttpGet] [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) 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); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Bamboo); var init = loadKit(ModInit.Bamboo);
if (!init.enable) if (!init.enable)
return Forbid(); return Forbid();
var invoke = new BambooInvoke(init, hybridCache, OnLog, proxyManager); var invoke = new BambooInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch) if (checksearch)
{ {
if (AppInit.conf?.online?.checkOnlineSearch != true) if (!IsCheckOnlineSearchEnabled())
return OnError("bamboo", proxyManager); return OnError("bamboo", refresh_proxy: true);
var searchResults = await invoke.Search(title, original_title); var searchResults = await invoke.Search(title, original_title);
if (searchResults != null && searchResults.Count > 0) if (searchResults != null && searchResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8"); return Content("data-json=", "text/plain; charset=utf-8");
return OnError("bamboo", proxyManager); return OnError("bamboo", refresh_proxy: true);
} }
string itemUrl = href; string itemUrl = href;
@ -51,14 +51,14 @@ namespace Bamboo.Controllers
{ {
var searchResults = await invoke.Search(title, original_title); var searchResults = await invoke.Search(title, original_title);
if (searchResults == null || searchResults.Count == 0) if (searchResults == null || searchResults.Count == 0)
return OnError("bamboo", proxyManager); return OnError("bamboo", refresh_proxy: true);
if (searchResults.Count > 1) if (searchResults.Count > 1)
{ {
var similar_tpl = new SimilarTpl(searchResults.Count); var similar_tpl = new SimilarTpl(searchResults.Count);
foreach (var res in searchResults) 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); 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); var series = await invoke.GetSeriesEpisodes(itemUrl);
if (series == null || (series.Sub.Count == 0 && series.Dub.Count == 0)) 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 voice_tpl = new VoiceTpl();
var episode_tpl = new EpisodeTpl(); var episode_tpl = new EpisodeTpl();
@ -88,13 +88,13 @@ namespace Bamboo.Controllers
foreach (var voice in availableVoices) 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); voice_tpl.Append(voice.name, voice.key == t, voiceLink);
} }
var selected = availableVoices.FirstOrDefault(v => v.key == t); var selected = availableVoices.FirstOrDefault(v => v.key == t);
if (selected.episodes == null || selected.episodes.Count == 0) if (selected.episodes == null || selected.episodes.Count == 0)
return OnError("bamboo", proxyManager); return OnError("bamboo", refresh_proxy: true);
int index = 1; int index = 1;
foreach (var ep in selected.episodes.OrderBy(e => e.Episode ?? int.MaxValue)) 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); var streams = await invoke.GetMovieStreams(itemUrl);
if (streams == null || streams.Count == 0) if (streams == null || streams.Count == 0)
return OnError("bamboo", proxyManager); return OnError("bamboo", refresh_proxy: true);
var movie_tpl = new MovieTpl(title, original_title); var movie_tpl = new MovieTpl(title, original_title);
for (int i = 0; i < streams.Count; i++) for (int i = 0; i < streams.Count; i++)
@ -166,5 +166,38 @@ namespace Bamboo.Controllers
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&'); cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned; 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.Engine;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting; using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
@ -22,9 +23,9 @@ using System.Threading.Tasks;
namespace Bamboo 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 OnlinesSettings Bamboo;
public static bool ApnHostProvided; public static bool ApnHostProvided;
@ -38,7 +39,7 @@ namespace Bamboo
/// <summary> /// <summary>
/// модуль загружен /// модуль загружен
/// </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" } 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); bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
conf.Remove("apn"); conf.Remove("apn");
conf.Remove("apn_host"); 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.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Shared.Models; using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace Bamboo namespace Bamboo
{ {
public class OnlineApi public class OnlineApi : IModuleOnline
{ {
public static List<(string name, string url, string plugin, int index)> Invoke( public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
{ {
long.TryParse(args.id, out long tmdbid); 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); 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; var init = ModInit.Bamboo;
if (init.enable && !init.rip) if (init.enable && !init.rip)
@ -34,11 +30,10 @@ namespace Bamboo
return online; return online;
} }
string url = init.overridehost; if (UpdateService.IsDisconnected())
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) init.overridehost = null;
url = $"{host}/bamboo";
online.Add((init.displayname, url, "bamboo", init.displayindex)); online.Add(new ModuleOnlineItem(init, "bamboo"));
} }
return online; return online;

View File

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

View File

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

View File

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

View File

@ -2,8 +2,8 @@ using JackTor.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Shared; using Shared;
using Shared.Engine;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Net.Mime; using System.Net.Mime;
@ -15,9 +15,9 @@ using System.Threading.Tasks;
namespace JackTor 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; public static JackTorSettings JackTor;
@ -30,7 +30,7 @@ namespace JackTor
/// <summary> /// <summary>
/// Модуль завантажено. /// Модуль завантажено.
/// </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) 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>(); JackTor = conf.ToObject<JackTorSettings>();
if (string.IsNullOrWhiteSpace(JackTor.jackett)) if (string.IsNullOrWhiteSpace(JackTor.jackett))
@ -76,7 +76,45 @@ namespace JackTor
JackTor.host = JackTor.jackett; 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 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 Microsoft.Extensions.Caching.Memory;
using Shared.Models; using Shared.Models;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace JackTor namespace JackTor
{ {
public class OnlineApi public class OnlineApi : IModuleOnline
{ {
public static List<(string name, string url, string plugin, int index)> Invoke( public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
{ {
long.TryParse(args.id, out long tmdbid); 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); 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( 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)
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; var init = ModInit.JackTor;
if (init.enable && !init.rip) if (init.enable && !init.rip)
{ {
string url = init.overridehost; if (UpdateService.IsDisconnected())
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) init.overridehost = null;
url = $"{host}/jacktor";
online.Add((init.displayname, url, "jacktor", init.displayindex)); online.Add(new ModuleOnlineItem(init, "jacktor"));
} }
return online; return online;

View File

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

View File

@ -25,6 +25,23 @@ namespace Shared.Engine
return true; 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) public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
{ {
if (init == null) if (init == null)
@ -37,8 +54,13 @@ namespace Shared.Engine
return; return;
} }
if (string.IsNullOrWhiteSpace(host)) host = NormalizeHost(host);
host = DefaultHost; if (host == null)
{
init.apnstream = false;
init.apn = null;
return;
}
if (init.apn == null) if (init.apn == null)
init.apn = new ApnConf(); init.apn = new ApnConf();
@ -82,5 +104,13 @@ namespace Shared.Engine
return $"{host.TrimEnd('/')}/{url}"; 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] [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) 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); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.KlonFUN); var init = loadKit(ModInit.KlonFUN);
if (!init.enable) if (!init.enable)
return Forbid(); return Forbid();
var invoke = new KlonFUNInvoke(init, hybridCache, OnLog, proxyManager); TryEnableMagicApn(init);
var invoke = new KlonFUNInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch) if (checksearch)
{ {
if (AppInit.conf?.online?.checkOnlineSearch != true) if (!IsCheckOnlineSearchEnabled())
return OnError("klonfun", proxyManager); return OnError("klonfun", refresh_proxy: true);
var checkResults = await invoke.Search(imdb_id, title, original_title); var checkResults = await invoke.Search(imdb_id, title, original_title);
if (checkResults != null && checkResults.Count > 0) if (checkResults != null && checkResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8"); return Content("data-json=", "text/plain; charset=utf-8");
return OnError("klonfun", proxyManager); return OnError("klonfun", refresh_proxy: true);
} }
string itemUrl = href; string itemUrl = href;
@ -50,14 +52,14 @@ namespace KlonFUN.Controllers
{ {
var searchResults = await invoke.Search(imdb_id, title, original_title); var searchResults = await invoke.Search(imdb_id, title, original_title);
if (searchResults == null || searchResults.Count == 0) if (searchResults == null || searchResults.Count == 0)
return OnError("klonfun", proxyManager); return OnError("klonfun", refresh_proxy: true);
if (searchResults.Count > 1) if (searchResults.Count > 1)
{ {
var similarTpl = new SimilarTpl(searchResults.Count); var similarTpl = new SimilarTpl(searchResults.Count);
foreach (SearchResult result in searchResults) 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); 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)) if (item == null || string.IsNullOrWhiteSpace(item.PlayerUrl))
{ {
OnLog($"KlonFUN: не знайдено iframe-плеєр для {itemUrl}"); OnLog($"KlonFUN: не знайдено iframe-плеєр для {itemUrl}");
return OnError("klonfun", proxyManager); return OnError("klonfun", refresh_proxy: true);
} }
string contentTitle = !string.IsNullOrWhiteSpace(title) ? title : item.Title; string contentTitle = !string.IsNullOrWhiteSpace(title) ? title : item.Title;
@ -87,7 +89,7 @@ namespace KlonFUN.Controllers
{ {
var serialStructure = await invoke.GetSerialStructure(item.PlayerUrl); var serialStructure = await invoke.GetSerialStructure(item.PlayerUrl);
if (serialStructure == null || serialStructure.Voices.Count == 0) if (serialStructure == null || serialStructure.Voices.Count == 0)
return OnError("klonfun", proxyManager); return OnError("klonfun", refresh_proxy: true);
if (s == -1) if (s == -1)
{ {
@ -118,12 +120,12 @@ namespace KlonFUN.Controllers
} }
if (seasons.Count == 0) if (seasons.Count == 0)
return OnError("klonfun", proxyManager); return OnError("klonfun", refresh_proxy: true);
var seasonTpl = new SeasonTpl(seasons.Count); var seasonTpl = new SeasonTpl(seasons.Count);
foreach (int seasonNumber in seasons) 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)) if (!string.IsNullOrWhiteSpace(t))
link += $"&t={HttpUtility.UrlEncode(t)}"; link += $"&t={HttpUtility.UrlEncode(t)}";
@ -140,7 +142,7 @@ namespace KlonFUN.Controllers
.ToList(); .ToList();
if (voicesForSeason.Count == 0) if (voicesForSeason.Count == 0)
return OnError("klonfun", proxyManager); return OnError("klonfun", refresh_proxy: true);
var selectedVoiceForSeason = voicesForSeason var selectedVoiceForSeason = voicesForSeason
.FirstOrDefault(v => !string.IsNullOrWhiteSpace(t) && v.Key.Equals(t, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(v => !string.IsNullOrWhiteSpace(t) && v.Key.Equals(t, StringComparison.OrdinalIgnoreCase))
@ -149,12 +151,12 @@ namespace KlonFUN.Controllers
var voiceTpl = new VoiceTpl(voicesForSeason.Count); var voiceTpl = new VoiceTpl(voicesForSeason.Count);
foreach (var voice in voicesForSeason) 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); voiceTpl.Append(voice.DisplayName, voice.Key.Equals(selectedVoiceForSeason.Key, StringComparison.OrdinalIgnoreCase), voiceLink);
} }
if (!selectedVoiceForSeason.Seasons.TryGetValue(s, out List<SerialEpisode> episodes) || episodes.Count == 0) 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); var episodeTpl = new EpisodeTpl(episodes.Count);
foreach (SerialEpisode episode in episodes.OrderBy(e => e.Number)) foreach (SerialEpisode episode in episodes.OrderBy(e => e.Number))
@ -177,7 +179,7 @@ namespace KlonFUN.Controllers
{ {
var streams = await invoke.GetMovieStreams(item.PlayerUrl); var streams = await invoke.GetMovieStreams(item.PlayerUrl);
if (streams == null || streams.Count == 0) if (streams == null || streams.Count == 0)
return OnError("klonfun", proxyManager); return OnError("klonfun", refresh_proxy: true);
var movieTpl = new MovieTpl(contentTitle, contentOriginalTitle, streams.Count); var movieTpl = new MovieTpl(contentTitle, contentOriginalTitle, streams.Count);
for (int i = 0; i < streams.Count; i++) for (int i = 0; i < streams.Count; i++)
@ -217,6 +219,24 @@ namespace KlonFUN.Controllers
return HostStreamProxy(init, link); 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) private static string StripLampacArgs(string url)
{ {
if (string.IsNullOrWhiteSpace(url)) if (string.IsNullOrWhiteSpace(url))
@ -232,5 +252,38 @@ namespace KlonFUN.Controllers
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&'); cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned; 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"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType> <OutputType>library</OutputType>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
</PropertyGroup> </PropertyGroup>

View File

@ -28,13 +28,15 @@ namespace KlonFUN
private readonly IHybridCache _hybridCache; private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog; private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager; 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; _init = init;
_hybridCache = hybridCache; _hybridCache = hybridCache;
_onLog = onLog; _onLog = onLog;
_proxyManager = proxyManager; _proxyManager = proxyManager;
_httpHydra = httpHydra;
} }
public async Task<List<SearchResult>> Search(string imdbId, string title, string originalTitle) public async Task<List<SearchResult>> Search(string imdbId, string title, string originalTitle)
@ -108,7 +110,7 @@ namespace KlonFUN
try try
{ {
var headers = DefaultHeaders(); 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)) if (string.IsNullOrWhiteSpace(html))
return null; return null;
@ -362,7 +364,7 @@ namespace KlonFUN
var headers = DefaultHeaders(); var headers = DefaultHeaders();
string form = $"do=search&subaction=search&story={HttpUtility.UrlEncode(query)}"; 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)) if (string.IsNullOrWhiteSpace(html))
return null; return null;
@ -465,7 +467,7 @@ namespace KlonFUN
requestUrl = ApnHelper.WrapUrl(_init, playerUrl); requestUrl = ApnHelper.WrapUrl(_init, playerUrl);
var headers = DefaultHeaders(); 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) private static JArray ParsePlayerArray(string html)
@ -699,16 +701,28 @@ namespace KlonFUN
return null; 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) 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) if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub); return TimeSpan.FromMinutes(rhub);
int ctime = AppInit.conf.mikrotik int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
? mikrotik
: AppInit.conf.multiaccess
? init != null && init.cache_time > 0 ? init.cache_time : multiaccess
: home;
if (ctime > multiaccess) if (ctime > multiaccess)
ctime = multiaccess; ctime = multiaccess;

View File

@ -3,6 +3,7 @@ using Shared;
using Shared.Engine; using Shared.Engine;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
@ -13,17 +14,19 @@ using System.Security.Authentication;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Shared.Models.Events;
namespace KlonFUN 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 bool ApnHostProvided;
public static string MagicApnAshdiHost;
public static OnlinesSettings Settings public static ModuleConfig Settings
{ {
get => KlonFUN; get => KlonFUN;
set => KlonFUN = value; set => KlonFUN = value;
@ -32,9 +35,18 @@ namespace KlonFUN
/// <summary> /// <summary>
/// Модуль завантажено. /// Модуль завантажено.
/// </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", displayname = "KlonFUN",
displayindex = 0, 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); 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");
conf.Remove("apn_host"); conf.Remove("apn_host");
KlonFUN = conf.ToObject<OnlinesSettings>(); KlonFUN = conf.ToObject<ModuleConfig>();
if (hasApn) if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, KlonFUN); ApnHelper.ApplyInitConf(apnEnabled, apnHost, KlonFUN);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost); ApnHostProvided = ApnHelper.IsEnabled(KlonFUN);
if (hasApn && apnEnabled) if (ApnHostProvided)
{ {
KlonFUN.streamproxy = false; KlonFUN.streamproxy = false;
} }
@ -65,9 +85,45 @@ namespace KlonFUN
KlonFUN.apnstream = false; KlonFUN.apnstream = false;
KlonFUN.apn = null; KlonFUN.apn = null;
} }
}
// Додаємо підтримку "уточнити пошук". private static void RegisterWithSearch(string plugin)
AppInit.conf.online.with_search.Add("klonfun"); {
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.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Shared.Models; using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace KlonFUN namespace KlonFUN
{ {
public class OnlineApi public class OnlineApi : IModuleOnline
{ {
public static List<(string name, string url, string plugin, int index)> Invoke( public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
{ {
long.TryParse(args.id, out long tmdbid); 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); 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; var init = ModInit.KlonFUN;
if (init.enable && !init.rip) if (init.enable && !init.rip)
{ {
string url = init.overridehost; if (UpdateService.IsDisconnected())
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) init.overridehost = null;
url = $"{host}/klonfun";
online.Add((init.displayname, url, "klonfun", init.displayindex)); online.Add(new ModuleOnlineItem(init, "klonfun"));
} }
return online; return online;

View File

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

View File

@ -23,20 +23,37 @@ namespace Shared.Engine
if (apnToken.Type == JTokenType.Boolean) if (apnToken.Type == JTokenType.Boolean)
{ {
enabled = apnToken.Value<bool>(); enabled = apnToken.Value<bool>();
host = conf.Value<string>("apn_host"); host = NormalizeHost(conf.Value<string>("apn_host"));
return true; return true;
} }
if (apnToken.Type == JTokenType.String) if (apnToken.Type == JTokenType.String)
{ {
host = apnToken.Value<string>(); host = NormalizeHost(apnToken.Value<string>());
enabled = !string.IsNullOrWhiteSpace(host); enabled = host != null;
return true; return true;
} }
return false; 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) public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
{ {
if (init == null) if (init == null)
@ -49,8 +66,13 @@ namespace Shared.Engine
return; return;
} }
if (string.IsNullOrWhiteSpace(host)) host = NormalizeHost(host);
host = DefaultHost; if (host == null)
{
init.apnstream = false;
init.apn = null;
return;
}
if (init.apn == null) if (init.apn == null)
init.apn = new ApnConf(); init.apn = new ApnConf();
@ -94,5 +116,13 @@ namespace Shared.Engine
return $"{host.TrimEnd('/')}/{url}"; 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 namespace Makhno
{ {
[Route("makhno")] [Route("lite/makhno")]
public class MakhnoController : BaseOnlineController public class MakhnoController : BaseOnlineController
{ {
private readonly ProxyManager proxyManager; private readonly ProxyManager proxyManager;
@ -28,7 +28,7 @@ namespace Makhno
{ {
if (checksearch) if (checksearch)
{ {
if (AppInit.conf?.online?.checkOnlineSearch != true) if (!IsCheckOnlineSearchEnabled())
return OnError(); return OnError();
return Content("data-json=", "text/plain; charset=utf-8"); return Content("data-json=", "text/plain; charset=utf-8");
@ -36,14 +36,15 @@ namespace Makhno
await UpdateService.ConnectAsync(host); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Makhno); var init = loadKit(ModInit.Makhno);
if (!init.enable) if (!init.enable)
return OnError(); return OnError();
TryEnableMagicApn(init);
Initialization(init); Initialization(init);
OnLog($"Makhno: {title} (serial={serial}, s={s}, season={season}, t={t})"); 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); var resolved = await ResolvePlaySource(imdb_id, serial, invoke);
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl)) if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
@ -61,14 +62,15 @@ namespace Makhno
{ {
await UpdateService.ConnectAsync(host); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Makhno); var init = loadKit(ModInit.Makhno);
if (!init.enable) if (!init.enable)
return OnError(); return OnError();
TryEnableMagicApn(init);
Initialization(init); Initialization(init);
OnLog($"Makhno Play: {title} (s={s}, season={season}, t={t}, episodeId={episodeId}) play={play}"); 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); var resolved = await ResolvePlaySource(imdb_id, serial: 1, invoke);
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl)) if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
return OnError(); return OnError();
@ -119,14 +121,15 @@ namespace Makhno
{ {
await UpdateService.ConnectAsync(host); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Makhno); var init = loadKit(ModInit.Makhno);
if (!init.enable) if (!init.enable)
return OnError(); return OnError();
TryEnableMagicApn(init);
Initialization(init); Initialization(init);
OnLog($"Makhno PlayMovie: {title} ({year}) play={play}"); 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); var resolved = await ResolvePlaySource(imdb_id, serial: 0, invoke);
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl)) if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
return OnError(); return OnError();
@ -267,7 +270,7 @@ namespace Makhno
string voiceParam = seasonVoiceIndex.HasValue ? $"&t={seasonVoiceIndex.Value}" : string.Empty; string voiceParam = seasonVoiceIndex.HasValue ? $"&t={seasonVoiceIndex.Value}" : string.Empty;
string seasonName = seasonItem.HasValue ? seasonItem.Value.Season?.Title ?? $"Сезон {seasonNumber}" : $"Сезон {seasonNumber}"; 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()); season_tpl.Append(seasonName, link, seasonNumber.ToString());
} }
@ -337,7 +340,7 @@ namespace Makhno
string voiceParam = seasonVoiceIndexForTpl.HasValue ? $"&t={seasonVoiceIndexForTpl.Value}" : string.Empty; string voiceParam = seasonVoiceIndexForTpl.HasValue ? $"&t={seasonVoiceIndexForTpl.Value}" : string.Empty;
string seasonName = seasonItem.HasValue ? seasonItem.Value.Season?.Title ?? $"Сезон {seasonNumber}" : $"Сезон {seasonNumber}"; 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()); seasonTplForVoice.Append(seasonName, link, seasonNumber.ToString());
} }
@ -354,11 +357,11 @@ namespace Makhno
bool sameSeasonSet = seasonsForVoice.Select(s => s.Number).ToHashSet().SetEquals(selectedVoiceSeasonSet); bool sameSeasonSet = seasonsForVoice.Select(s => s.Number).ToHashSet().SetEquals(selectedVoiceSeasonSet);
if (hasRequestedSeason && sameSeasonSet) 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 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(); bool isActive = selectedVoice == i.ToString();
@ -374,7 +377,7 @@ namespace Makhno
bool hasRequestedSeason = seasonsForVoice.Any(s => s.Number == requestedSeason); bool hasRequestedSeason = seasonsForVoice.Any(s => s.Number == requestedSeason);
if (!hasRequestedSeason) 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)); return UpdateService.Validate(Redirect(redirectUrl));
} }
@ -512,10 +515,61 @@ namespace Makhno
return HostStreamProxy(init, link); 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 private class ResolveResult
{ {
public string PlayUrl { get; set; } public string PlayUrl { get; set; }
public bool IsSerial { 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"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType> <OutputType>library</OutputType>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
</PropertyGroup> </PropertyGroup>

View File

@ -24,13 +24,15 @@ namespace Makhno
private readonly IHybridCache _hybridCache; private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog; private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager; 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; _init = init;
_hybridCache = hybridCache; _hybridCache = hybridCache;
_onLog = onLog; _onLog = onLog;
_proxyManager = proxyManager; _proxyManager = proxyManager;
_httpHydra = httpHydra;
} }
public async Task<string> GetWormholePlay(string imdbId) public async Task<string> GetWormholePlay(string imdbId)
@ -46,7 +48,7 @@ namespace Makhno
new HeadersModel("User-Agent", Http.UserAgent) 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)) if (string.IsNullOrWhiteSpace(response))
return null; return null;
@ -84,7 +86,7 @@ namespace Makhno
_onLog($"Makhno getting player data from: {requestUrl}"); _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)) if (string.IsNullOrEmpty(response))
return null; return null;
@ -526,6 +528,14 @@ namespace Makhno
return normalized; 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 private class WormholeResponse
{ {
public string play { get; set; } public string play { get; set; }

View File

@ -4,6 +4,7 @@ using Shared.Engine;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting; using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
@ -21,12 +22,13 @@ using System.Threading.Tasks;
namespace Makhno 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 OnlinesSettings Makhno;
public static bool ApnHostProvided; public static bool ApnHostProvided;
public static string MagicApnAshdiHost;
public static OnlinesSettings Settings public static OnlinesSettings Settings
{ {
@ -37,7 +39,7 @@ namespace Makhno
/// <summary> /// <summary>
/// модуль загружен /// модуль загружен
/// </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) 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" } 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); bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
MagicApnAshdiHost = ApnHelper.TryGetMagicAshdiHost(conf);
conf.Remove("magic_apn");
if (hasApn) if (hasApn)
{ {
conf.Remove("apn"); conf.Remove("apn");
@ -61,8 +71,8 @@ namespace Makhno
Makhno = conf.ToObject<OnlinesSettings>(); Makhno = conf.ToObject<OnlinesSettings>();
if (hasApn) if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, Makhno); ApnHelper.ApplyInitConf(apnEnabled, apnHost, Makhno);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost); ApnHostProvided = ApnHelper.IsEnabled(Makhno);
if (hasApn && apnEnabled) if (ApnHostProvided)
{ {
Makhno.streamproxy = false; 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.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Shared.Models; using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace Makhno namespace Makhno
{ {
public class OnlineApi public class OnlineApi : IModuleOnline
{ {
public static List<(string name, string url, string plugin, int index)> Invoke( public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
{ {
long.TryParse(args.id, out long tmdbid); 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); 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; var init = ModInit.Makhno;
if (init.enable && !init.rip) if (init.enable && !init.rip)
{ {
string url = init.overridehost; if (UpdateService.IsDisconnected())
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) init.overridehost = null;
url = $"{host}/makhno";
online.Add((init.displayname, url, "makhno", init.displayindex)); online.Add(new ModuleOnlineItem(init, "makhno"));
} }
return online; return online;

View File

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

View File

@ -25,6 +25,23 @@ namespace Shared.Engine
return true; 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) public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
{ {
if (init == null) if (init == null)
@ -37,8 +54,13 @@ namespace Shared.Engine
return; return;
} }
if (string.IsNullOrWhiteSpace(host)) host = NormalizeHost(host);
host = DefaultHost; if (host == null)
{
init.apnstream = false;
init.apn = null;
return;
}
if (init.apn == null) if (init.apn == null)
init.apn = new ApnConf(); init.apn = new ApnConf();
@ -82,5 +104,13 @@ namespace Shared.Engine
return $"{host.TrimEnd('/')}/{url}"; 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] [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) 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); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Mikai); var init = loadKit(ModInit.Mikai);
if (!init.enable) if (!init.enable)
return Forbid(); return Forbid();
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager); TryEnableMagicApn(init);
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager, httpHydra);
if (checksearch) if (checksearch)
{ {
if (AppInit.conf?.online?.checkOnlineSearch != true) if (!IsCheckOnlineSearchEnabled())
return OnError("mikai", _proxyManager); return OnError("mikai", refresh_proxy: true);
var checkResults = await invoke.Search(title, original_title, year); var checkResults = await invoke.Search(title, original_title, year);
if (checkResults != null && checkResults.Count > 0) if (checkResults != null && checkResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8"); 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}"); 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); var searchResults = await invoke.Search(title, original_title, year);
if (searchResults == null || searchResults.Count == 0) if (searchResults == null || searchResults.Count == 0)
return OnError("mikai", _proxyManager); return OnError("mikai", refresh_proxy: true);
var selected = searchResults.FirstOrDefault(); var selected = searchResults.FirstOrDefault();
if (selected == null) if (selected == null)
return OnError("mikai", _proxyManager); return OnError("mikai", refresh_proxy: true);
var details = await invoke.GetDetails(selected.Id); var details = await invoke.GetDetails(selected.Id);
if (details == null || details.Players == null || details.Players.Count == 0) 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)); bool isSerial = serial == 1 || (serial == -1 && !string.Equals(details.Format, "movie", StringComparison.OrdinalIgnoreCase));
var seasonDetails = await CollectSeasonDetails(details, invoke); var seasonDetails = await CollectSeasonDetails(details, invoke);
var voices = BuildVoices(seasonDetails); var voices = BuildVoices(seasonDetails);
if (voices.Count == 0) if (voices.Count == 0)
return OnError("mikai", _proxyManager); return OnError("mikai", refresh_proxy: true);
string displayTitle = title ?? details.Details?.Names?.Name ?? original_title; string displayTitle = title ?? details.Details?.Names?.Name ?? original_title;
@ -81,14 +82,14 @@ namespace Mikai.Controllers
.ToList(); .ToList();
if (seasonNumbers.Count == 0) if (seasonNumbers.Count == 0)
return OnError("mikai", _proxyManager); return OnError("mikai", refresh_proxy: true);
if (s == -1) if (s == -1)
{ {
var seasonTpl = new SeasonTpl(seasonNumbers.Count); var seasonTpl = new SeasonTpl(seasonNumbers.Count);
foreach (var seasonNumber in seasonNumbers) 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) if (restrictByVoice)
link += $"&t={HttpUtility.UrlEncode(t)}"; link += $"&t={HttpUtility.UrlEncode(t)}";
seasonTpl.Append($"{seasonNumber}", link, seasonNumber.ToString()); seasonTpl.Append($"{seasonNumber}", link, seasonNumber.ToString());
@ -104,7 +105,7 @@ namespace Mikai.Controllers
.ToList(); .ToList();
if (!voicesForSeason.Any()) if (!voicesForSeason.Any())
return OnError("mikai", _proxyManager); return OnError("mikai", refresh_proxy: true);
if (string.IsNullOrEmpty(t)) if (string.IsNullOrEmpty(t))
t = voicesForSeason[0].Key; t = voicesForSeason[0].Key;
@ -118,7 +119,7 @@ namespace Mikai.Controllers
{ {
var targetSeasonSet = GetSeasonSet(voice.Value); var targetSeasonSet = GetSeasonSet(voice.Value);
bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet); 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) if (sameSeasonSet)
voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.Key)}"; voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
else else
@ -128,7 +129,7 @@ namespace Mikai.Controllers
if (!voices.ContainsKey(t) || !voices[t].Seasons.ContainsKey(s)) 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); return Redirect(redirectUrl);
} }
@ -143,7 +144,7 @@ namespace Mikai.Controllers
if (NeedsResolve(voices[t].ProviderName, streamLink)) 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"); episodeTpl.Append(episodeName, displayTitle, s.ToString(), ep.Number.ToString(), accsArgs(callUrl), "call");
} }
else else
@ -177,14 +178,14 @@ namespace Mikai.Controllers
foreach (var ashdiStream in ashdiStreams) foreach (var ashdiStream in ashdiStreams)
{ {
string optionName = $"{voice.DisplayName} {ashdiStream.title}"; 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"); movieTpl.Append(optionName, accsArgs(ashdiCallUrl), "call");
} }
continue; 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"); movieTpl.Append(voice.DisplayName, accsArgs(callUrl), "call");
} }
else else
@ -195,31 +196,32 @@ namespace Mikai.Controllers
} }
if (movieTpl.data == null || movieTpl.data.Count == 0) if (movieTpl.data == null || movieTpl.data.Count == 0)
return OnError("mikai", _proxyManager); return OnError("mikai", refresh_proxy: true);
return rjson return rjson
? Content(movieTpl.ToJson(), "application/json; charset=utf-8") ? Content(movieTpl.ToJson(), "application/json; charset=utf-8")
: Content(movieTpl.ToHtml(), "text/html; 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) public async Task<ActionResult> Play(string url, string title = null, int serial = 0)
{ {
await UpdateService.ConnectAsync(host); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Mikai); var init = loadKit(ModInit.Mikai);
if (!init.enable) if (!init.enable)
return Forbid(); return Forbid();
TryEnableMagicApn(init);
if (string.IsNullOrEmpty(url)) 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}"); OnLog($"Mikai Play: url={url}, serial={serial}");
string streamLink = await invoke.ResolveVideoUrl(url, serial == 1); string streamLink = await invoke.ResolveVideoUrl(url, serial == 1);
if (string.IsNullOrEmpty(streamLink)) if (string.IsNullOrEmpty(streamLink))
return OnError("mikai", _proxyManager); return OnError("mikai", refresh_proxy: true);
List<HeadersModel> streamHeaders = null; List<HeadersModel> streamHeaders = null;
bool forceProxy = false; bool forceProxy = false;
@ -462,5 +464,56 @@ namespace Mikai.Controllers
return HostStreamProxy(init, link, headers: headers, force_streamproxy: forceProxy); 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"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType> <OutputType>library</OutputType>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
</PropertyGroup> </PropertyGroup>

View File

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

View File

@ -3,6 +3,7 @@ using Shared;
using Shared.Engine; using Shared.Engine;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting; using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
@ -22,12 +23,13 @@ using System.Threading.Tasks;
namespace Mikai 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 OnlinesSettings Mikai;
public static bool ApnHostProvided; public static bool ApnHostProvided;
public static string MagicApnAshdiHost;
public static OnlinesSettings Settings public static OnlinesSettings Settings
{ {
@ -38,7 +40,7 @@ namespace Mikai
/// <summary> /// <summary>
/// модуль загружен /// модуль загружен
/// </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); 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");
conf.Remove("apn_host"); conf.Remove("apn_host");
Mikai = conf.ToObject<OnlinesSettings>(); Mikai = conf.ToObject<OnlinesSettings>();
if (hasApn) if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, Mikai); ApnHelper.ApplyInitConf(apnEnabled, apnHost, Mikai);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost); ApnHostProvided = ApnHelper.IsEnabled(Mikai);
if (hasApn && apnEnabled) if (ApnHostProvided)
{ {
Mikai.streamproxy = false; 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); 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 Microsoft.Extensions.Caching.Memory;
using Shared.Models; using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace Mikai namespace Mikai
{ {
public class OnlineApi public class OnlineApi : IModuleOnline
{ {
public static List<(string name, string url, string plugin, int index)> Invoke( public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
{ {
long.TryParse(args.id, out long tmdbid); 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); 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; var init = ModInit.Mikai;
// Визначаємо isAnime згідно стандарту Lampac (Deepwiki):
// isanime = true якщо original_language == "ja" або "zh"
bool hasLang = !string.IsNullOrEmpty(original_language); bool hasLang = !string.IsNullOrEmpty(original_language);
bool isanime = hasLang && (original_language == "ja" || original_language == "zh"); bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
// Mikai — аніме-провайдер. Додаємо його:
// - при загальному пошуку (serial == -1), або
// - якщо контент визначений як аніме (isanime), або
// - якщо мова невідома (відсутній original_language)
if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang)) if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang))
{ {
string url = init.overridehost; if (UpdateService.IsDisconnected())
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) init.overridehost = null;
url = $"{host}/mikai";
online.Add((init.displayname, url, "mikai", init.displayindex)); online.Add(new ModuleOnlineItem(init, "mikai"));
} }
return online; return online;

View File

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

View File

@ -25,35 +25,35 @@ namespace NMoonAnime.Controllers
} }
[HttpGet] [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) 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); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.NMoonAnime); var init = loadKit(ModInit.NMoonAnime);
if (!init.enable) if (!init.enable)
return Forbid(); 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); string effectiveMalId = ResolveMalId(mal_id, kinopoisk_id, source);
if (checksearch) if (checksearch)
{ {
if (AppInit.conf?.online?.checkOnlineSearch != true) if (!IsCheckOnlineSearchEnabled())
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
var checkResults = await invoke.Search(imdb_id, effectiveMalId, title, year); var checkResults = await invoke.Search(imdb_id, effectiveMalId, title, year);
if (checkResults != null && checkResults.Count > 0) if (checkResults != null && checkResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8"); 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}"); 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); var seasons = await invoke.Search(imdb_id, effectiveMalId, title, year);
if (seasons == null || seasons.Count == 0) if (seasons == null || seasons.Count == 0)
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
bool isSeries = serial == 1; bool isSeries = serial == 1;
NMoonAnimeSeasonContent firstSeasonData = null; NMoonAnimeSeasonContent firstSeasonData = null;
@ -62,7 +62,7 @@ namespace NMoonAnime.Controllers
{ {
firstSeasonData = await invoke.GetSeasonContent(seasons[0]); firstSeasonData = await invoke.GetSeasonContent(seasons[0]);
if (firstSeasonData == null || firstSeasonData.Voices.Count == 0) if (firstSeasonData == null || firstSeasonData.Voices.Count == 0)
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
isSeries = firstSeasonData.IsSeries; isSeries = firstSeasonData.IsSeries;
} }
@ -75,22 +75,22 @@ namespace NMoonAnime.Controllers
return await RenderMovie(invoke, seasons, title, original_title, firstSeasonData, rjson); 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) public async Task<ActionResult> Play(string file, string title = null)
{ {
await UpdateService.ConnectAsync(host); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.NMoonAnime); var init = loadKit(ModInit.NMoonAnime);
if (!init.enable) if (!init.enable)
return Forbid(); return Forbid();
if (string.IsNullOrWhiteSpace(file)) 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); var streams = invoke.ParseStreams(file);
if (streams == null || streams.Count == 0) if (streams == null || streams.Count == 0)
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
if (streams.Count == 1) if (streams.Count == 1)
{ {
@ -107,7 +107,7 @@ namespace NMoonAnime.Controllers
} }
if (!streamQuality.Any()) if (!streamQuality.Any())
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
var first = streamQuality.Firts(); var first = streamQuality.Firts();
string json = VideoTpl.ToJson("play", first.link, title ?? string.Empty, streamquality: streamQuality); string json = VideoTpl.ToJson("play", first.link, title ?? string.Empty, streamquality: streamQuality);
@ -133,7 +133,7 @@ namespace NMoonAnime.Controllers
.ToList(); .ToList();
if (orderedSeasons.Count == 0) if (orderedSeasons.Count == 0)
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
if (selectedSeason == -1) if (selectedSeason == -1)
{ {
@ -154,14 +154,14 @@ namespace NMoonAnime.Controllers
var currentSeason = orderedSeasons.FirstOrDefault(s => s.SeasonNumber == selectedSeason) ?? orderedSeasons[0]; var currentSeason = orderedSeasons.FirstOrDefault(s => s.SeasonNumber == selectedSeason) ?? orderedSeasons[0];
var seasonData = await invoke.GetSeasonContent(currentSeason); var seasonData = await invoke.GetSeasonContent(currentSeason);
if (seasonData == null) if (seasonData == null)
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
var voices = seasonData.Voices var voices = seasonData.Voices
.Where(v => v != null && v.Episodes != null && v.Episodes.Count > 0) .Where(v => v != null && v.Episodes != null && v.Episodes.Count > 0)
.ToList(); .ToList();
if (voices.Count == 0) if (voices.Count == 0)
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
int activeVoiceIndex = ParseVoiceIndex(selectedVoice, voices.Count); int activeVoiceIndex = ParseVoiceIndex(selectedVoice, voices.Count);
var voiceTpl = new VoiceTpl(voices.Count); var voiceTpl = new VoiceTpl(voices.Count);
@ -180,7 +180,7 @@ namespace NMoonAnime.Controllers
.ToList(); .ToList();
if (episodes.Count == 0) if (episodes.Count == 0)
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
string displayTitle = !string.IsNullOrWhiteSpace(title) string displayTitle = !string.IsNullOrWhiteSpace(title)
? title ? title
@ -193,7 +193,7 @@ namespace NMoonAnime.Controllers
{ {
int episodeNumber = episode.Number <= 0 ? 1 : episode.Number; int episodeNumber = episode.Number <= 0 ? 1 : episode.Number;
string episodeName = string.IsNullOrWhiteSpace(episode.Name) ? $"Епізод {episodeNumber}" : episode.Name; 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"); episodeTpl.Append(episodeName, displayTitle, currentSeason.SeasonNumber.ToString(), episodeNumber.ToString(), accsArgs(callUrl), "call");
} }
@ -218,14 +218,14 @@ namespace NMoonAnime.Controllers
.FirstOrDefault(); .FirstOrDefault();
if (currentSeason == null) if (currentSeason == null)
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
NMoonAnimeSeasonContent seasonData = firstSeasonData; NMoonAnimeSeasonContent seasonData = firstSeasonData;
if (seasonData == null || !string.Equals(seasonData.Url, currentSeason.Url, StringComparison.OrdinalIgnoreCase)) if (seasonData == null || !string.Equals(seasonData.Url, currentSeason.Url, StringComparison.OrdinalIgnoreCase))
seasonData = await invoke.GetSeasonContent(currentSeason); seasonData = await invoke.GetSeasonContent(currentSeason);
if (seasonData == null || seasonData.Voices.Count == 0) if (seasonData == null || seasonData.Voices.Count == 0)
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
string displayTitle = !string.IsNullOrWhiteSpace(title) string displayTitle = !string.IsNullOrWhiteSpace(title)
? title ? title
@ -249,13 +249,13 @@ namespace NMoonAnime.Controllers
continue; continue;
string voiceName = string.IsNullOrWhiteSpace(voice.Name) ? $"Озвучка {fallbackIndex}" : voice.Name; 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"); movieTpl.Append(voiceName, accsArgs(callUrl), "call");
fallbackIndex++; fallbackIndex++;
} }
if (movieTpl.IsEmpty) if (movieTpl.IsEmpty)
return OnError("nmoonanime", proxyManager); return OnError("nmoonanime", refresh_proxy: true);
return rjson return rjson
? Content(movieTpl.ToJson(), "application/json; charset=utf-8") ? 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) 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(); 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($"&kinopoisk_id={kinopoiskId}");
url.Append($"&title={HttpUtility.UrlEncode(title)}"); url.Append($"&title={HttpUtility.UrlEncode(title)}");
url.Append($"&original_title={HttpUtility.UrlEncode(originalTitle)}"); url.Append($"&original_title={HttpUtility.UrlEncode(originalTitle)}");
@ -353,5 +353,38 @@ namespace NMoonAnime.Controllers
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&'); cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned; 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;
using Shared.Engine; using Shared.Engine;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using System; using System;
using System.Net.Http; using System.Net.Http;
@ -16,9 +17,9 @@ using System.Threading.Tasks;
namespace NMoonAnime 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; public static OnlinesSettings NMoonAnime;
@ -33,7 +34,7 @@ namespace NMoonAnime
/// <summary> /// <summary>
/// Модуль завантажено. /// Модуль завантажено.
/// </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) 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); bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
conf.Remove("apn"); conf.Remove("apn");
conf.Remove("apn_host"); conf.Remove("apn_host");
@ -68,7 +69,45 @@ namespace NMoonAnime
NMoonAnime.apn = null; 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"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<OutputType>library</OutputType> <OutputType>library</OutputType>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
</PropertyGroup> </PropertyGroup>

View File

@ -22,6 +22,7 @@ namespace NMoonAnime
private readonly IHybridCache _hybridCache; private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog; private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager; private readonly ProxyManager _proxyManager;
private readonly HttpHydra _httpHydra;
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{ {
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true
@ -35,12 +36,13 @@ namespace NMoonAnime
private static readonly UTF8Encoding _utf8Strict = new UTF8Encoding(false, true); private static readonly UTF8Encoding _utf8Strict = new UTF8Encoding(false, true);
private static readonly Encoding _latin1 = Encoding.GetEncoding("ISO-8859-1"); 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; _init = init;
_hybridCache = hybridCache; _hybridCache = hybridCache;
_onLog = onLog; _onLog = onLog;
_proxyManager = proxyManager; _proxyManager = proxyManager;
_httpHydra = httpHydra;
} }
public async Task<List<NMoonAnimeSeasonRef>> Search(string imdbId, string malId, string title, int year) public async Task<List<NMoonAnimeSeasonRef>> Search(string imdbId, string malId, string title, int year)
@ -64,7 +66,7 @@ namespace NMoonAnime
continue; continue;
_onLog($"NMoonAnime: пошук через {searchUrl}"); _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)) if (string.IsNullOrWhiteSpace(json))
continue; continue;
@ -108,7 +110,7 @@ namespace NMoonAnime
try try
{ {
_onLog($"NMoonAnime: завантаження сезону {season.Url}"); _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)) if (string.IsNullOrWhiteSpace(html))
return null; 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) 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) if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub); 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) if (ctime > multiaccess)
ctime = multiaccess; ctime = multiaccess;

View File

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

View File

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

View File

@ -1,4 +1,4 @@
# Ukraine online source for Lampac # Ukraine online source for Lampac NextGen
## Sources ## Sources
### TVShows and Movies ### TVShows and Movies
@ -76,15 +76,17 @@ modules - optional, if not specified, all modules from the repository will be in
] ]
}, },
"displayindex": 1, "displayindex": 1,
"apn": true, "magic_apn": {
"apn_host": "domaine.com/{encodeurl}" "ashdi": "https://tut.im/proxy.php?url={encodeurl}"
}
} }
``` ```
Parameter compatibility: Parameter compatibility:
- `webcorshost` + `useproxy`: work together (parsing via CORS host, and network output can go through a proxy with `useproxy`). - `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 `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`) ## JackTor config example (`init.conf`)

View File

@ -24,27 +24,27 @@ namespace StarLight.Controllers
} }
[HttpGet] [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) 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); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.StarLight); var init = loadKit(ModInit.StarLight);
if (!init.enable) if (!init.enable)
return Forbid(); return Forbid();
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager); var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch) if (checksearch)
{ {
if (AppInit.conf?.online?.checkOnlineSearch != true) if (!IsCheckOnlineSearchEnabled())
return OnError("starlight", proxyManager); return OnError("starlight", refresh_proxy: true);
var searchResults = await invoke.Search(title, original_title); var searchResults = await invoke.Search(title, original_title);
if (searchResults != null && searchResults.Count > 0) if (searchResults != null && searchResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8"); return Content("data-json=", "text/plain; charset=utf-8");
return OnError("starlight", proxyManager); return OnError("starlight", refresh_proxy: true);
} }
string itemUrl = href; string itemUrl = href;
@ -52,14 +52,14 @@ namespace StarLight.Controllers
{ {
var searchResults = await invoke.Search(title, original_title); var searchResults = await invoke.Search(title, original_title);
if (searchResults == null || searchResults.Count == 0) if (searchResults == null || searchResults.Count == 0)
return OnError("starlight", proxyManager); return OnError("starlight", refresh_proxy: true);
if (searchResults.Count > 1) if (searchResults.Count > 1)
{ {
var similar_tpl = new SimilarTpl(searchResults.Count); var similar_tpl = new SimilarTpl(searchResults.Count);
foreach (var res in searchResults) 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); 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); var project = await invoke.GetProject(itemUrl);
if (project == null) if (project == null)
return OnError("starlight", proxyManager); return OnError("starlight", refresh_proxy: true);
if (serial == 1 && project.Seasons.Count > 0) if (serial == 1 && project.Seasons.Count > 0)
{ {
@ -82,7 +82,7 @@ namespace StarLight.Controllers
{ {
var seasonInfo = project.Seasons[i]; var seasonInfo = project.Seasons[i];
string seasonName = string.IsNullOrEmpty(seasonInfo.Title) ? $"Сезон {i + 1}" : seasonInfo.Title; 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()); season_tpl.Append(seasonName, link, i.ToString());
} }
@ -90,13 +90,13 @@ namespace StarLight.Controllers
} }
if (s < 0 || s >= project.Seasons.Count) if (s < 0 || s >= project.Seasons.Count)
return OnError("starlight", proxyManager); return OnError("starlight", refresh_proxy: true);
var season = project.Seasons[s]; var season = project.Seasons[s];
string seasonSlug = season.Slug; string seasonSlug = season.Slug;
var episodes = invoke.GetEpisodes(project, seasonSlug); var episodes = invoke.GetEpisodes(project, seasonSlug);
if (episodes == null || episodes.Count == 0) if (episodes == null || episodes.Count == 0)
return OnError("starlight", proxyManager); return OnError("starlight", refresh_proxy: true);
var episode_tpl = new EpisodeTpl(); var episode_tpl = new EpisodeTpl();
int index = 1; int index = 1;
@ -114,7 +114,7 @@ namespace StarLight.Controllers
continue; continue;
string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {index}" : ep.Title; 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"); episode_tpl.Append(episodeName, title ?? original_title, seasonNumber, index.ToString("D2"), accsArgs(callUrl), "call");
index++; index++;
} }
@ -128,9 +128,9 @@ namespace StarLight.Controllers
hash = project.Episodes.FirstOrDefault(e => !string.IsNullOrEmpty(e.Hash))?.Hash; hash = project.Episodes.FirstOrDefault(e => !string.IsNullOrEmpty(e.Hash))?.Hash;
if (string.IsNullOrEmpty(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); var movie_tpl = new MovieTpl(title, original_title, 1);
movie_tpl.Append(string.IsNullOrEmpty(title) ? "StarLight" : title, accsArgs(callUrl), "call"); movie_tpl.Append(string.IsNullOrEmpty(title) ? "StarLight" : title, accsArgs(callUrl), "call");
@ -139,22 +139,22 @@ namespace StarLight.Controllers
} }
[HttpGet] [HttpGet]
[Route("starlight/play")] [Route("lite/starlight/play")]
async public Task<ActionResult> Play(string hash, string title) async public Task<ActionResult> Play(string hash, string title)
{ {
await UpdateService.ConnectAsync(host); await UpdateService.ConnectAsync(host);
if (string.IsNullOrEmpty(hash)) 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) if (!init.enable)
return Forbid(); 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); var result = await invoke.ResolveStream(hash);
if (result == null || string.IsNullOrEmpty(result.Stream)) if (result == null || string.IsNullOrEmpty(result.Stream))
return OnError("starlight", proxyManager); return OnError("starlight", refresh_proxy: true);
string videoTitle = title ?? result.Name ?? ""; 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; 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;
using Shared.Engine; using Shared.Engine;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting; using Microsoft.CodeAnalysis.Scripting;
@ -22,9 +23,9 @@ using System.Threading.Tasks;
namespace StarLight 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 OnlinesSettings StarLight;
public static bool ApnHostProvided; public static bool ApnHostProvided;
@ -38,7 +39,7 @@ namespace StarLight
/// <summary> /// <summary>
/// модуль загружен /// модуль загружен
/// </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" } 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); bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
conf.Remove("apn"); conf.Remove("apn");
conf.Remove("apn_host"); 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.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Shared.Models; using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace StarLight namespace StarLight
{ {
public class OnlineApi public class OnlineApi : IModuleOnline
{ {
public static List<(string name, string url, string plugin, int index)> Invoke( public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
{ {
long.TryParse(args.id, out long tmdbid); 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); 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)) if (!string.Equals(original_language, "uk", StringComparison.OrdinalIgnoreCase))
return online; return online;
@ -31,11 +27,10 @@ namespace StarLight
var init = ModInit.StarLight; var init = ModInit.StarLight;
if (init.enable && !init.rip) if (init.enable && !init.rip)
{ {
string url = init.overridehost; if (UpdateService.IsDisconnected())
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) init.overridehost = null;
url = $"{host}/starlight";
online.Add((init.displayname, url, "starlight", init.displayindex)); online.Add(new ModuleOnlineItem(init, "starlight"));
} }
return online; return online;

View File

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

View File

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

View File

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

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; 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) public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
{ {
if (init == null) if (init == null)
@ -37,8 +54,13 @@ namespace Shared.Engine
return; return;
} }
if (string.IsNullOrWhiteSpace(host)) host = NormalizeHost(host);
host = DefaultHost; if (host == null)
{
init.apnstream = false;
init.apn = null;
return;
}
if (init.apn == null) if (init.apn == null)
init.apn = new ApnConf(); init.apn = new ApnConf();
@ -82,5 +104,13 @@ namespace Shared.Engine
return $"{host.TrimEnd('/')}/{url}"; 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] [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) 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); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.UaFlix); if (await IsRequestBlocked(rch: false))
if (await IsBadInitialization(init)) return badInitMsg;
return Forbid();
var init = this.init;
TryEnableMagicApn(init);
OnLog($"=== UAFLIX INDEX START ==="); OnLog($"=== UAFLIX INDEX START ===");
OnLog($"Uaflix Index: title={title}, serial={serial}, s={s}, play={play}, href={href}, checksearch={checksearch}"); 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: kinopoisk_id={kinopoisk_id}, imdb_id={imdb_id}, id={id}");
OnLog($"Uaflix Index: year={year}, source={source}, t={t}, e={e}, rjson={rjson}"); OnLog($"Uaflix Index: year={year}, source={source}, t={t}, e={e}, rjson={rjson}");
var auth = new UaflixAuth(init, memoryCache, OnLog, proxyManager); 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 - повертаємо спеціальну відповідь для валідації // Обробка параметра checksearch - повертаємо спеціальну відповідь для валідації
if (checksearch) if (checksearch)
{ {
if (AppInit.conf?.online?.checkOnlineSearch != true) if (!IsCheckOnlineSearchEnabled())
return OnError("uaflix", proxyManager); return OnError("uaflix", refresh_proxy: true);
try try
{ {
@ -63,13 +64,13 @@ namespace Uaflix.Controllers
OnLog("checksearch: Контент не знайдено"); OnLog("checksearch: Контент не знайдено");
OnLog("=== RETURN: checksearch OnError ==="); OnLog("=== RETURN: checksearch OnError ===");
return OnError("uaflix", proxyManager); return OnError("uaflix", refresh_proxy: true);
} }
catch (Exception ex) catch (Exception ex)
{ {
OnLog($"checksearch: помилка - {ex.Message}"); OnLog($"checksearch: помилка - {ex.Message}");
OnLog("=== RETURN: checksearch exception OnError ==="); 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)) if (string.IsNullOrWhiteSpace(urlToParse))
{ {
OnLog("=== RETURN: play missing url OnError ==="); OnLog("=== RETURN: play missing url OnError ===");
return OnError("uaflix", proxyManager); return OnError("uaflix", refresh_proxy: true);
} }
var playResult = await invoke.ParseEpisode(urlToParse); var playResult = await invoke.ParseEpisode(urlToParse);
@ -91,7 +92,7 @@ namespace Uaflix.Controllers
} }
OnLog("=== RETURN: play no streams ==="); OnLog("=== RETURN: play no streams ===");
return OnError("uaflix", proxyManager); return OnError("uaflix", refresh_proxy: true);
} }
// Якщо є episode_url але немає play=true, це виклик для отримання інформації про стрім (для method: 'call') // Якщо є episode_url але немає play=true, це виклик для отримання інформації про стрім (для method: 'call')
@ -109,7 +110,7 @@ namespace Uaflix.Controllers
} }
OnLog("=== RETURN: call method no streams ==="); OnLog("=== RETURN: call method no streams ===");
return OnError("uaflix", proxyManager); return OnError("uaflix", refresh_proxy: true);
} }
string filmUrl = href; string filmUrl = href;
@ -121,7 +122,7 @@ namespace Uaflix.Controllers
{ {
OnLog("No search results found"); OnLog("No search results found");
OnLog("=== RETURN: no search results OnError ==="); 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); var selectedResult = invoke.SelectBestSearchResult(searchResults, title, original_title, year);
@ -142,7 +143,7 @@ namespace Uaflix.Controllers
var similar_tpl = new SimilarTpl(orderedResults.Count); var similar_tpl = new SimilarTpl(orderedResults.Count);
foreach (var res in orderedResults) 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 y = res.Year > 0 ? res.Year.ToString() : string.Empty;
string details = res.Category switch string details = res.Category switch
{ {
@ -162,97 +163,50 @@ namespace Uaflix.Controllers
if (serial == 1) if (serial == 1)
{ {
// Агрегуємо всі озвучки з усіх плеєрів // s == -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: Вибір сезону
if (s == -1) if (s == -1)
{ {
List<int> allSeasons; var seasonIndex = await invoke.GetSeasonIndex(filmUrl);
VoiceInfo tVoice = null; var seasons = seasonIndex?.Seasons?.Keys
bool restrictByVoice = !string.IsNullOrEmpty(t) && structure.Voices.TryGetValue(t, out tVoice) && IsAshdiVoice(tVoice); .Distinct()
if (restrictByVoice) .OrderBy(sn => sn)
.ToList();
if (seasons == null || seasons.Count == 0)
{ {
allSeasons = GetSeasonSet(tVoice).OrderBy(sn => sn).ToList(); OnLog("No seasons found in season index");
OnLog($"Ashdi voice selected (t='{t}'), seasons count={allSeasons.Count}"); OnLog("=== RETURN: no seasons OnError ===");
} return OnError("uaflix", refresh_proxy: true);
else
{
allSeasons = structure.Voices
.SelectMany(v => GetSeasonSet(v.Value))
.Distinct()
.OrderBy(sn => sn)
.ToList();
} }
OnLog($"Found {allSeasons.Count} seasons in structure: {string.Join(", ", allSeasons)}"); var season_tpl = new SeasonTpl(seasons.Count);
foreach (int season in seasons)
// Перевіряємо чи сезони містять валідні епізоди з файлами
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 episodesInSeason = structure.Voices.Values 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)}";
.Where(v => v.Seasons.ContainsKey(season)) if (!string.IsNullOrWhiteSpace(t))
.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)
link += $"&t={HttpUtility.UrlEncode(t)}"; link += $"&t={HttpUtility.UrlEncode(t)}";
season_tpl.Append($"{season}", link, season.ToString()); season_tpl.Append($"{season}", link, season.ToString());
OnLog($"Added season {season} to template");
} }
OnLog($"Returning season template with {seasonsWithValidEpisodes.Count} seasons"); OnLog($"=== RETURN: season template ({seasons.Count} seasons) ===");
return Content(
var htmlContent = rjson ? season_tpl.ToJson() : season_tpl.ToHtml(); rjson ? season_tpl.ToJson() : season_tpl.ToHtml(),
OnLog($"Season template response length: {htmlContent.Length}"); rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8"
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");
} }
// 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 var voicesForSeason = structure.Voices
.Where(v => v.Value.Seasons.ContainsKey(s))
.Select(v => new { DisplayName = v.Key, Info = v.Value }) .Select(v => new { DisplayName = v.Key, Info = v.Value })
.ToList(); .ToList();
@ -260,7 +214,7 @@ namespace Uaflix.Controllers
{ {
OnLog($"No voices found for season {s}"); OnLog($"No voices found for season {s}");
OnLog("=== RETURN: no voices for season OnError ==="); 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}"); 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 з усіма озвучками // Створюємо VoiceTpl з усіма озвучками
var voice_tpl = new VoiceTpl(); var voice_tpl = new VoiceTpl();
var selectedVoiceInfo = structure.Voices[t];
var selectedSeasonSet = GetSeasonSet(selectedVoiceInfo);
bool selectedIsAshdi = IsAshdiVoice(selectedVoiceInfo);
foreach (var voice in voicesForSeason) foreach (var voice in voicesForSeason)
{ {
bool targetIsAshdi = IsAshdiVoice(voice.Info); 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)}";
var targetSeasonSet = GetSeasonSet(voice.Info); voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.DisplayName)}";
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)}";
bool isActive = voice.DisplayName == t; bool isActive = voice.DisplayName == t;
voice_tpl.Append(voice.DisplayName, isActive, voiceLink); voice_tpl.Append(voice.DisplayName, isActive, voiceLink);
} }
OnLog($"Created VoiceTpl with {voicesForSeason.Count} voices, active: {t}"); 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)) var episodes = selectedVoice.Seasons[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 episode_tpl = new EpisodeTpl(); var episode_tpl = new EpisodeTpl();
int appendedEpisodes = 0;
foreach (var ep in episodes) 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 // Для zetvideo-vod повертаємо URL епізоду з методом call
// Для ashdi/zetvideo-serial повертаємо готове посилання з play // Для ashdi/zetvideo-serial повертаємо готове посилання з play
var voice = structure.Voices[t]; var voice = selectedVoice;
if (voice.PlayerType == "zetvideo-vod" || voice.PlayerType == "ashdi-vod") if (voice.PlayerType == "zetvideo-vod" || voice.PlayerType == "ashdi-vod")
{ {
// Для zetvideo-vod та ashdi-vod використовуємо URL епізоду для виклику // Для zetvideo-vod та ashdi-vod використовуємо URL епізоду для виклику
// Потрібно передати URL епізоду в інший параметр, щоб не плутати з play=true // Потрібно передати 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( episode_tpl.Append(
name: ep.Title, name: episodeTitle,
title: title, title: title,
s: s.ToString(), s: s.ToString(),
e: ep.Number.ToString(), e: ep.Number.ToString(),
@ -350,16 +292,25 @@ namespace Uaflix.Controllers
// Для багатосерійних плеєрів (ashdi-serial, zetvideo-serial) - пряме відтворення // Для багатосерійних плеєрів (ashdi-serial, zetvideo-serial) - пряме відтворення
string playUrl = BuildStreamUrl(init, ep.File); string playUrl = BuildStreamUrl(init, ep.File);
episode_tpl.Append( episode_tpl.Append(
name: ep.Title, name: episodeTitle,
title: title, title: title,
s: s.ToString(), s: s.ToString(),
e: ep.Number.ToString(), e: ep.Number.ToString(),
link: playUrl 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 разом // Повертаємо VoiceTpl + EpisodeTpl разом
episode_tpl.Append(voice_tpl); episode_tpl.Append(voice_tpl);
@ -378,7 +329,7 @@ namespace Uaflix.Controllers
// Fallback: якщо жоден з умов не виконався // Fallback: якщо жоден з умов не виконався
OnLog($"Fallback: s={s}, t={t}"); OnLog($"Fallback: s={s}, t={t}");
OnLog("=== RETURN: fallback OnError ==="); OnLog("=== RETURN: fallback OnError ===");
return OnError("uaflix", proxyManager); return OnError("uaflix", refresh_proxy: true);
} }
else // Фільм else // Фільм
{ {
@ -386,7 +337,7 @@ namespace Uaflix.Controllers
if (playResult?.streams == null || playResult.streams.Count == 0) if (playResult?.streams == null || playResult.streams.Count == 0)
{ {
OnLog("=== RETURN: movie no streams ==="); 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); 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) if (tpl.data == null || tpl.data.Count == 0)
{ {
OnLog("=== RETURN: movie template empty ==="); OnLog("=== RETURN: movie template empty ===");
return OnError("uaflix", proxyManager); return OnError("uaflix", refresh_proxy: true);
} }
OnLog("=== RETURN: movie template ==="); OnLog("=== RETURN: movie template ===");
@ -435,6 +386,24 @@ namespace Uaflix.Controllers
return HostStreamProxy(init, link); 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) private static string StripLampacArgs(string url)
{ {
if (string.IsNullOrEmpty(url)) if (string.IsNullOrEmpty(url))
@ -451,23 +420,37 @@ namespace Uaflix.Controllers
return cleaned; return cleaned;
} }
private static bool IsAshdiVoice(VoiceInfo voice) private static bool IsCheckOnlineSearchEnabled()
{ {
if (voice == null || string.IsNullOrEmpty(voice.PlayerType)) try
return false; {
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) System.Console.WriteLine(message);
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();
} }
} }
} }

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;
using Shared.Engine; using Shared.Engine;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Net.Mime; using System.Net.Mime;
@ -16,13 +17,14 @@ using Uaflix.Models;
namespace Uaflix 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 UaflixSettings UaFlix;
public static bool ApnHostProvided; public static bool ApnHostProvided;
public static string MagicApnAshdiHost;
public static UaflixSettings Settings public static UaflixSettings Settings
{ {
@ -33,7 +35,7 @@ namespace Uaflix
/// <summary> /// <summary>
/// Модуль завантажено. /// Модуль завантажено.
/// </summary> /// </summary>
public static void loaded(InitspaceModel initspace) public void Loaded(InitspaceModel initspace)
{ {
UaFlix = new UaflixSettings("Uaflix", "https://uafix.net", streamproxy: false, useproxy: false) 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); 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");
conf.Remove("apn_host"); conf.Remove("apn_host");
UaFlix = conf.ToObject<UaflixSettings>(); UaFlix = conf.ToObject<UaflixSettings>();
@ -62,8 +72,8 @@ namespace Uaflix
if (hasApn) if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, UaFlix); ApnHelper.ApplyInitConf(apnEnabled, apnHost, UaFlix);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost); ApnHostProvided = ApnHelper.IsEnabled(UaFlix);
if (hasApn && apnEnabled) if (ApnHostProvided)
{ {
UaFlix.streamproxy = false; 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>(); 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; } public int TotalPages { get; set; }
@ -16,4 +19,4 @@ namespace Uaflix.Models
public List<EpisodeLinkInfo> Episodes { get; set; } = new List<EpisodeLinkInfo>(); 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 Microsoft.Extensions.Caching.Memory;
using Shared.Models; using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module; using Shared.Models.Module;
using System.Collections.Generic; using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
namespace Uaflix using System.Threading.Tasks;
{
public class OnlineApi namespace Uaflix
{
public class OnlineApi : IModuleOnline
{ {
public static List<(string name, string url, string plugin, int index)> Invoke( public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
{ {
long.TryParse(args.id, out long tmdbid); 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); 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; var init = ModInit.UaFlix;
if (init.enable && !init.rip) if (init.enable && !init.rip)
{ {
string url = init.overridehost; if (UpdateService.IsDisconnected())
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) init.overridehost = null;
url = $"{host}/uaflix";
online.Add((init.displayname, url, "uaflix", init.displayindex)); online.Add(new ModuleOnlineItem(init, "uaflix"));
} }
return online; return online;

View File

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

View File

@ -28,14 +28,16 @@ namespace Uaflix
private readonly Action<string> _onLog; private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager; private readonly ProxyManager _proxyManager;
private readonly UaflixAuth _auth; 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; _init = init;
_hybridCache = hybridCache; _hybridCache = hybridCache;
_onLog = onLog; _onLog = onLog;
_proxyManager = proxyManager; _proxyManager = proxyManager;
_auth = auth; _auth = auth;
_httpHydra = httpHydra;
} }
string AshdiRequestUrl(string url) string AshdiRequestUrl(string url)
@ -76,7 +78,6 @@ namespace Uaflix
if (string.IsNullOrWhiteSpace(url)) if (string.IsNullOrWhiteSpace(url))
return null; return null;
string requestUrl = _init.cors(url);
bool withAuth = ShouldUseAuth(url); bool withAuth = ShouldUseAuth(url);
var requestHeaders = headers != null ? new List<HeadersModel>(headers) : new List<HeadersModel>(); var requestHeaders = headers != null ? new List<HeadersModel>(headers) : new List<HeadersModel>();
@ -86,6 +87,26 @@ namespace Uaflix
_auth.ApplyCookieHeader(requestHeaders, cookie); _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, var response = await Http.BaseGet(requestUrl,
headers: requestHeaders, headers: requestHeaders,
timeoutSeconds: timeoutSeconds, timeoutSeconds: timeoutSeconds,
@ -800,6 +821,440 @@ namespace Uaflix
#endregion #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) 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); bool allowAnime = IsAnimeRequest(title, original_title, original_language, source);
@ -1790,7 +2245,7 @@ namespace Uaflix
if (init != null && init.rhub && rhub != -1) if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub); 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) if (ctime > multiaccess)
ctime = multiaccess; ctime = multiaccess;
@ -1805,9 +2260,9 @@ namespace Uaflix
if (init != null && init.rhub && rhub != -1) if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub); 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 (init != null && ctime > init.cache_time && init.cache_time > 0) if (ctime > multiaccess)
ctime = init.cache_time; ctime = multiaccess;
return TimeSpan.FromMinutes(ctime); return TimeSpan.FromMinutes(ctime);
} }

View File

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

View File

@ -1,4 +1,3 @@
using Shared.Engine;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq; using System.Linq;
@ -22,20 +21,20 @@ namespace Unimay.Controllers
} }
[HttpGet] [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) 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); await UpdateService.ConnectAsync(host);
var init = await loadKit(ModInit.Unimay); if (await IsRequestBlocked(rch: false))
if (await IsBadInitialization(init, rch: false))
return badInitMsg; 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 (checksearch)
{ {
if (AppInit.conf?.online?.checkOnlineSearch != true) if (!IsCheckOnlineSearchEnabled())
return OnError("unimay"); return OnError("unimay");
var searchResults = await invoke.Search(title, original_title, serial); var searchResults = await invoke.Search(title, original_title, serial);
@ -169,5 +168,38 @@ namespace Unimay.Controllers
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&'); cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned; 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.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting; using Newtonsoft.Json;
using Microsoft.Extensions.Caching.Memory; using Newtonsoft.Json.Linq;
using Shared.Models; using Shared;
using Shared.Models.Events; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Shared.Models.Online.Settings;
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Net.Mime; using System.Net.Mime;
@ -28,9 +18,9 @@ using System.Threading.Tasks;
namespace Unimay 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; public static OnlinesSettings Unimay;
@ -43,7 +33,7 @@ namespace Unimay
/// <summary> /// <summary>
/// модуль загружен /// модуль загружен
/// </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" } 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); 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 Microsoft.Extensions.Caching.Memory;
using Shared.Models; using Shared.Models;
using Shared.Models.Base;
using Shared.Models.Module; using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace Unimay namespace Unimay
{ {
public class OnlineApi public class OnlineApi : IModuleOnline
{ {
public static List<(string name, string url, string plugin, int index)> Invoke( public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
HttpContext httpContext,
IMemoryCache memoryCache,
RequestModel requestInfo,
string host,
OnlineEventsModel args)
{ {
long.TryParse(args.id, out long tmdbid); 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); 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; var init = ModInit.Unimay;
// Визначення isAnime згідно стандарту Lampac (Deepwiki):
// isanime = true якщо original_language == "ja" або "zh"
bool hasLang = !string.IsNullOrEmpty(original_language); bool hasLang = !string.IsNullOrEmpty(original_language);
bool isanime = hasLang && (original_language == "ja" || original_language == "zh"); bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
// Unimay — аніме-провайдер. Додаємо якщо:
// - загальний пошук (serial == -1), або
// - контент є аніме (isanime), або
// - мова невідома (немає original_language)
if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang)) if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang))
{ {
string url = init.overridehost; if (UpdateService.IsDisconnected())
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected()) init.overridehost = null;
url = $"{host}/unimay";
online.Add((init.displayname, url, "unimay", init.displayindex)); online.Add(new ModuleOnlineItem(init, "unimay"));
} }
return online; return online;

View File

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

View File

@ -6,7 +6,6 @@ using Shared.Models.Online.Settings;
using Shared.Models; using Shared.Models;
using System.Linq; using System.Linq;
using Unimay.Models; using Unimay.Models;
using Shared.Engine;
using System.Net; using System.Net;
using System.Text; using System.Text;
@ -18,13 +17,15 @@ namespace Unimay
private ProxyManager _proxyManager; private ProxyManager _proxyManager;
private IHybridCache _hybridCache; private IHybridCache _hybridCache;
private Action<string> _onLog; 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; _init = init;
_hybridCache = hybridCache; _hybridCache = hybridCache;
_onLog = onLog; _onLog = onLog;
_proxyManager = proxyManager; _proxyManager = proxyManager;
_httpHydra = httpHydra;
} }
public async Task<SearchResponse> Search(string title, string original_title, int serial) 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}"; string searchUrl = $"{_init.host}/release/search?page=0&page_size=10&title={searchQuery}";
var headers = httpHeaders(_init); 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) if (root == null || root.Content == null || root.Content.Count == 0)
{ {
@ -69,7 +70,7 @@ namespace Unimay
string releaseUrl = $"{_init.host}/release?code={code}"; string releaseUrl = $"{_init.host}/release?code={code}";
var headers = httpHeaders(_init); 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) if (root == null)
{ {
@ -103,7 +104,7 @@ namespace Unimay
} }
string itemTitle = item.Names?.Ukr ?? item.Names?.Eng ?? item.Title; 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)); results.Add((itemTitle, item.Year, item.Type, releaseUrl));
} }
@ -116,7 +117,7 @@ namespace Unimay
return (null, null); return (null, null);
var movieEpisode = releaseDetail.Playlist[0]; 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; string movieTitle = movieEpisode.Title ?? title;
return (movieTitle, movieLink); 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) 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"); 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)) 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 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)); 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) 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) if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub); 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) if (ctime > multiaccess)
ctime = multiaccess; ctime = multiaccess;

View File

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