mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-04-16 09:22:21 +00:00
Compare commits
10 Commits
76fcbc8fbf
...
b8f3ea7568
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8f3ea7568 | ||
|
|
ff90f149f0 | ||
|
|
b1a7ce510d | ||
|
|
0aed459fab | ||
|
|
31549455ee | ||
|
|
0cb8412036 | ||
|
|
fc7ddf2668 | ||
|
|
ee5fae6581 | ||
|
|
312be86e27 | ||
|
|
6aece92fd0 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -11,3 +11,7 @@
|
||||
.DS_Store
|
||||
AGENTS.md
|
||||
/planing/
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
.vscode/settings.json
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -23,13 +23,15 @@ namespace AnimeON
|
||||
private IHybridCache _hybridCache;
|
||||
private Action<string> _onLog;
|
||||
private ProxyManager _proxyManager;
|
||||
private readonly HttpHydra _httpHydra;
|
||||
|
||||
public AnimeONInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||
public AnimeONInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
|
||||
{
|
||||
_init = init;
|
||||
_hybridCache = hybridCache;
|
||||
_onLog = onLog;
|
||||
_proxyManager = proxyManager;
|
||||
_httpHydra = httpHydra;
|
||||
}
|
||||
|
||||
string AshdiRequestUrl(string url)
|
||||
@ -61,7 +63,7 @@ namespace AnimeON
|
||||
string searchUrl = $"{_init.host}/api/anime/search?text={System.Web.HttpUtility.UrlEncode(query)}";
|
||||
|
||||
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {searchUrl}");
|
||||
string searchJson = await Http.Get(_init.cors(searchUrl), headers: headers, proxy: _proxyManager.Get());
|
||||
string searchJson = await HttpGet(searchUrl, headers);
|
||||
if (string.IsNullOrEmpty(searchJson))
|
||||
return null;
|
||||
|
||||
@ -124,7 +126,7 @@ namespace AnimeON
|
||||
string fundubsUrl = $"{_init.host}/api/player/{animeId}/translations";
|
||||
|
||||
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {fundubsUrl}");
|
||||
string fundubsJson = await Http.Get(_init.cors(fundubsUrl), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get());
|
||||
string fundubsJson = await HttpGet(fundubsUrl, new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) });
|
||||
if (string.IsNullOrEmpty(fundubsJson))
|
||||
return null;
|
||||
|
||||
@ -150,7 +152,7 @@ namespace AnimeON
|
||||
string episodesUrl = $"{_init.host}/api/player/{animeId}/episodes?take=100&skip=-1&playerId={playerId}&translationId={fundubId}";
|
||||
|
||||
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {episodesUrl}");
|
||||
string episodesJson = await Http.Get(_init.cors(episodesUrl), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get());
|
||||
string episodesJson = await HttpGet(episodesUrl, new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) });
|
||||
if (string.IsNullOrEmpty(episodesJson))
|
||||
return null;
|
||||
|
||||
@ -169,7 +171,7 @@ namespace AnimeON
|
||||
};
|
||||
|
||||
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
|
||||
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
|
||||
string html = await HttpGet(requestUrl, headers);
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return null;
|
||||
|
||||
@ -206,7 +208,7 @@ namespace AnimeON
|
||||
|
||||
string requestUrl = AshdiRequestUrl(WithAshdiMultivoice(url, enable: !disableAshdiMultivoiceForVod));
|
||||
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
|
||||
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
|
||||
string html = await HttpGet(requestUrl, headers);
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return streams;
|
||||
|
||||
@ -263,7 +265,7 @@ namespace AnimeON
|
||||
string url = $"{_init.host}/api/player/{episodeId}/episode";
|
||||
|
||||
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {url}");
|
||||
string json = await Http.Get(_init.cors(url), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get());
|
||||
string json = await HttpGet(url, new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) });
|
||||
if (string.IsNullOrEmpty(json))
|
||||
return null;
|
||||
|
||||
@ -474,12 +476,21 @@ namespace AnimeON
|
||||
if (init != null && init.rhub && rhub != -1)
|
||||
return TimeSpan.FromMinutes(rhub);
|
||||
|
||||
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
|
||||
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
|
||||
if (ctime > multiaccess)
|
||||
ctime = multiaccess;
|
||||
|
||||
return TimeSpan.FromMinutes(ctime);
|
||||
}
|
||||
|
||||
private Task<string> HttpGet(string url, List<HeadersModel> headers)
|
||||
{
|
||||
if (_httpHydra != null)
|
||||
return _httpHydra.Get(url, newheaders: headers);
|
||||
|
||||
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||
}
|
||||
|
||||
public async Task<AnimeON.Models.AnimeONAggregatedStructure> AggregateSerialStructure(int animeId, int season)
|
||||
{
|
||||
string memKey = $"AnimeON:aggregated:{animeId}:{season}";
|
||||
|
||||
@ -25,6 +25,23 @@ namespace Shared.Engine
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string TryGetMagicAshdiHost(JObject conf)
|
||||
{
|
||||
if (conf == null || !conf.TryGetValue("magic_apn", out var magicToken) || magicToken == null)
|
||||
return null;
|
||||
|
||||
if (magicToken.Type == JTokenType.Boolean)
|
||||
return magicToken.Value<bool>() ? DefaultHost : null;
|
||||
|
||||
if (magicToken.Type == JTokenType.String)
|
||||
return NormalizeHost(magicToken.Value<string>());
|
||||
|
||||
if (magicToken.Type != JTokenType.Object)
|
||||
return null;
|
||||
|
||||
return NormalizeHost(((JObject)magicToken).Value<string>("ashdi"));
|
||||
}
|
||||
|
||||
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
|
||||
{
|
||||
if (init == null)
|
||||
@ -37,8 +54,13 @@ namespace Shared.Engine
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
host = DefaultHost;
|
||||
host = NormalizeHost(host);
|
||||
if (host == null)
|
||||
{
|
||||
init.apnstream = false;
|
||||
init.apn = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (init.apn == null)
|
||||
init.apn = new ApnConf();
|
||||
@ -82,5 +104,13 @@ namespace Shared.Engine
|
||||
|
||||
return $"{host.TrimEnd('/')}/{url}";
|
||||
}
|
||||
|
||||
private static string NormalizeHost(string host)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
return null;
|
||||
|
||||
return host.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,27 +27,28 @@ namespace AnimeON.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("animeon")]
|
||||
[Route("lite/animeon")]
|
||||
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, bool rjson = false, bool checksearch = false)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.AnimeON);
|
||||
var init = loadKit(ModInit.AnimeON);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
TryEnableMagicApn(init);
|
||||
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
|
||||
if (checksearch)
|
||||
{
|
||||
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||
return OnError("animeon", proxyManager);
|
||||
if (!IsCheckOnlineSearchEnabled())
|
||||
return OnError("animeon", refresh_proxy: true);
|
||||
|
||||
var checkSeasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial);
|
||||
if (checkSeasons != null && checkSeasons.Count > 0)
|
||||
return Content("data-json=", "text/plain; charset=utf-8");
|
||||
|
||||
return OnError("animeon", proxyManager);
|
||||
return OnError("animeon", refresh_proxy: true);
|
||||
}
|
||||
|
||||
OnLog($"AnimeON Index: title={title}, original_title={original_title}, serial={serial}, s={s}, t={t}, year={year}, imdb_id={imdb_id}, kp={kinopoisk_id}");
|
||||
@ -55,7 +56,7 @@ namespace AnimeON.Controllers
|
||||
var seasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial);
|
||||
OnLog($"AnimeON: search results = {seasons?.Count ?? 0}");
|
||||
if (seasons == null || seasons.Count == 0)
|
||||
return OnError("animeon", proxyManager);
|
||||
return OnError("animeon", refresh_proxy: true);
|
||||
|
||||
// [Refactoring] Використовується агрегована структура (AggregateSerialStructure) — попередній збір allOptions не потрібний
|
||||
|
||||
@ -81,7 +82,7 @@ namespace AnimeON.Controllers
|
||||
foreach (var item in seasonItems)
|
||||
{
|
||||
string seasonName = item.SeasonNumber.ToString();
|
||||
string link = $"{host}/animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={item.SeasonNumber}";
|
||||
string link = $"{host}/lite/animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={item.SeasonNumber}";
|
||||
season_tpl.Append(seasonName, link, seasonName);
|
||||
}
|
||||
OnLog($"AnimeON: return seasons count={seasonItems.Count}");
|
||||
@ -106,13 +107,13 @@ namespace AnimeON.Controllers
|
||||
selected = new { Anime = seasons[s], Index = s, SeasonNumber = seasons[s].Season > 0 ? seasons[s].Season : s + 1 };
|
||||
|
||||
if (selected == null)
|
||||
return OnError("animeon", proxyManager);
|
||||
return OnError("animeon", refresh_proxy: true);
|
||||
|
||||
var selectedAnime = selected.Anime;
|
||||
int selectedSeasonNumber = selected.SeasonNumber;
|
||||
var structure = await invoke.AggregateSerialStructure(selectedAnime.Id, selectedSeasonNumber);
|
||||
if (structure == null || !structure.Voices.Any())
|
||||
return OnError("animeon", proxyManager);
|
||||
return OnError("animeon", refresh_proxy: true);
|
||||
|
||||
OnLog($"AnimeON: voices found = {structure.Voices.Count}");
|
||||
var voiceItems = structure.Voices
|
||||
@ -135,14 +136,14 @@ namespace AnimeON.Controllers
|
||||
var voice_tpl = new VoiceTpl();
|
||||
foreach (var voice in voiceItems)
|
||||
{
|
||||
string voiceLink = $"{host}/animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
|
||||
string voiceLink = $"{host}/lite/animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
|
||||
bool isActive = voice.Key == t;
|
||||
voice_tpl.Append(voice.Display, isActive, voiceLink);
|
||||
}
|
||||
|
||||
// Перевірка вибраної озвучки
|
||||
if (!structure.Voices.ContainsKey(t))
|
||||
return OnError("animeon", proxyManager);
|
||||
return OnError("animeon", refresh_proxy: true);
|
||||
|
||||
var episode_tpl = new EpisodeTpl();
|
||||
var selectedVoiceInfo = structure.Voices[t];
|
||||
@ -179,7 +180,7 @@ namespace AnimeON.Controllers
|
||||
|
||||
if (string.IsNullOrEmpty(streamLink) && ep.EpisodeId > 0)
|
||||
{
|
||||
string callUrl = $"{host}/animeon/play?episode_id={ep.EpisodeId}&serial=1";
|
||||
string callUrl = $"{host}/lite/animeon/play?episode_id={ep.EpisodeId}&serial=1";
|
||||
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
|
||||
continue;
|
||||
}
|
||||
@ -189,7 +190,7 @@ namespace AnimeON.Controllers
|
||||
|
||||
if (needsResolve || streamLink.Contains("moonanime.art") || streamLink.Contains("ashdi.vip/vod"))
|
||||
{
|
||||
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}&serial=1";
|
||||
string callUrl = $"{host}/lite/animeon/play?url={HttpUtility.UrlEncode(streamLink)}&serial=1";
|
||||
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
|
||||
}
|
||||
else
|
||||
@ -212,12 +213,12 @@ namespace AnimeON.Controllers
|
||||
{
|
||||
var firstAnime = seasons.FirstOrDefault();
|
||||
if (firstAnime == null)
|
||||
return OnError("animeon", proxyManager);
|
||||
return OnError("animeon", refresh_proxy: true);
|
||||
|
||||
var fundubs = await invoke.GetFundubs(firstAnime.Id);
|
||||
OnLog($"AnimeON: movie fundubs count = {fundubs?.Count ?? 0}");
|
||||
if (fundubs == null || fundubs.Count == 0)
|
||||
return OnError("animeon", proxyManager);
|
||||
return OnError("animeon", refresh_proxy: true);
|
||||
|
||||
var tpl = new MovieTpl(title, original_title);
|
||||
|
||||
@ -251,7 +252,7 @@ namespace AnimeON.Controllers
|
||||
foreach (var ashdiStream in ashdiStreams)
|
||||
{
|
||||
string optionName = $"{translationName} {ashdiStream.title}";
|
||||
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(ashdiStream.link)}";
|
||||
string callUrl = $"{host}/lite/animeon/play?url={HttpUtility.UrlEncode(ashdiStream.link)}";
|
||||
tpl.Append(optionName, accsArgs(callUrl), "call");
|
||||
}
|
||||
continue;
|
||||
@ -260,7 +261,7 @@ namespace AnimeON.Controllers
|
||||
|
||||
if (needsResolve || streamLink.Contains("moonanime.art/iframe/") || streamLink.Contains("ashdi.vip/vod"))
|
||||
{
|
||||
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";
|
||||
string callUrl = $"{host}/lite/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";
|
||||
tpl.Append(translationName, accsArgs(callUrl), "call");
|
||||
}
|
||||
else
|
||||
@ -272,7 +273,7 @@ namespace AnimeON.Controllers
|
||||
|
||||
// Якщо не зібрали жодної опції — повертаємо помилку
|
||||
if (tpl.data == null || tpl.data.Count == 0)
|
||||
return OnError("animeon", proxyManager);
|
||||
return OnError("animeon", refresh_proxy: true);
|
||||
|
||||
OnLog("AnimeON: return movie options");
|
||||
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8");
|
||||
@ -283,7 +284,7 @@ namespace AnimeON.Controllers
|
||||
{
|
||||
string fundubsUrl = $"{init.host}/api/player/{animeId}/translations";
|
||||
|
||||
string fundubsJson = await Http.Get(init.cors(fundubsUrl), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
|
||||
string fundubsJson = await httpHydra.Get(fundubsUrl, newheaders: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
|
||||
if (string.IsNullOrEmpty(fundubsJson))
|
||||
return null;
|
||||
|
||||
@ -308,7 +309,7 @@ namespace AnimeON.Controllers
|
||||
{
|
||||
string episodesUrl = $"{init.host}/api/player/{animeId}/episodes?take=100&skip=-1&playerId={playerId}&translationId={fundubId}";
|
||||
|
||||
string episodesJson = await Http.Get(init.cors(episodesUrl), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
|
||||
string episodesJson = await httpHydra.Get(episodesUrl, newheaders: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
|
||||
if (string.IsNullOrEmpty(episodesJson))
|
||||
return null;
|
||||
|
||||
@ -332,7 +333,7 @@ namespace AnimeON.Controllers
|
||||
|
||||
string searchUrl = $"{init.host}/api/anime/search?text={HttpUtility.UrlEncode(query)}";
|
||||
|
||||
string searchJson = await Http.Get(init.cors(searchUrl), headers: headers);
|
||||
string searchJson = await httpHydra.Get(searchUrl, newheaders: headers);
|
||||
if (string.IsNullOrEmpty(searchJson))
|
||||
return null;
|
||||
|
||||
@ -373,16 +374,17 @@ namespace AnimeON.Controllers
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpGet("animeon/play")]
|
||||
[HttpGet("lite/animeon/play")]
|
||||
public async Task<ActionResult> Play(string url, int episode_id = 0, string title = null, int serial = 0)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.AnimeON);
|
||||
var init = loadKit(ModInit.AnimeON);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
TryEnableMagicApn(init);
|
||||
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
bool disableAshdiMultivoiceForVod = serial == 1;
|
||||
OnLog($"AnimeON Play: url={url}, episode_id={episode_id}, serial={serial}");
|
||||
|
||||
@ -398,13 +400,13 @@ namespace AnimeON.Controllers
|
||||
else
|
||||
{
|
||||
OnLog("AnimeON Play: empty url");
|
||||
return OnError("animeon", proxyManager);
|
||||
return OnError("animeon", refresh_proxy: true);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(streamLink))
|
||||
{
|
||||
OnLog("AnimeON Play: cannot extract stream");
|
||||
return OnError("animeon", proxyManager);
|
||||
return OnError("animeon", refresh_proxy: true);
|
||||
}
|
||||
|
||||
List<HeadersModel> streamHeaders = null;
|
||||
@ -462,5 +464,56 @@ namespace AnimeON.Controllers
|
||||
|
||||
return HostStreamProxy(init, link, headers: headers, force_streamproxy: forceProxy);
|
||||
}
|
||||
|
||||
private void TryEnableMagicApn(OnlinesSettings init)
|
||||
{
|
||||
if (init == null
|
||||
|| init.apn != null
|
||||
|| init.streamproxy
|
||||
|| string.IsNullOrWhiteSpace(ModInit.MagicApnAshdiHost))
|
||||
return;
|
||||
|
||||
string player = new RchClient(HttpContext, host, init, requestInfo).InfoConnected()?.player;
|
||||
bool useInnerPlayer = string.IsNullOrWhiteSpace(player)
|
||||
|| player.Equals("inner", StringComparison.OrdinalIgnoreCase);
|
||||
if (!useInnerPlayer)
|
||||
return;
|
||||
|
||||
ApnHelper.ApplyInitConf(true, ModInit.MagicApnAshdiHost, init);
|
||||
OnLog($"AnimeON: увімкнено magic_apn для Ashdi (player={player ?? "unknown"}).");
|
||||
}
|
||||
|
||||
private static bool IsCheckOnlineSearchEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (checkProp?.GetValue(conf) is bool enabled)
|
||||
return enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void OnLog(string message)
|
||||
{
|
||||
System.Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
AnimeON/GlobalUsings.cs
Normal file
4
AnimeON/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
global using Shared.Services;
|
||||
global using Shared.Services.Hybrid;
|
||||
global using Shared.Models.Base;
|
||||
global using AppInit = Shared.CoreInit;
|
||||
@ -3,6 +3,7 @@ using Newtonsoft.Json.Linq;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using Shared.Models.Online.Settings;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
@ -23,12 +24,13 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace AnimeON
|
||||
{
|
||||
public class ModInit
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 3.7;
|
||||
public static double Version => 4.0;
|
||||
|
||||
public static OnlinesSettings AnimeON;
|
||||
public static bool ApnHostProvided;
|
||||
public static string MagicApnAshdiHost;
|
||||
|
||||
public static OnlinesSettings Settings
|
||||
{
|
||||
@ -39,7 +41,7 @@ namespace AnimeON
|
||||
/// <summary>
|
||||
/// модуль загружен
|
||||
/// </summary>
|
||||
public static void loaded(InitspaceModel initspace)
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
|
||||
|
||||
@ -55,15 +57,23 @@ namespace AnimeON
|
||||
list = new string[] { "socks5://ip:port" }
|
||||
}
|
||||
};
|
||||
var conf = ModuleInvoke.Conf("AnimeON", AnimeON);
|
||||
var defaults = JObject.FromObject(AnimeON);
|
||||
defaults["magic_apn"] = new JObject()
|
||||
{
|
||||
["ashdi"] = ApnHelper.DefaultHost
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Init("AnimeON", defaults) ?? defaults;
|
||||
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||
MagicApnAshdiHost = ApnHelper.TryGetMagicAshdiHost(conf);
|
||||
conf.Remove("magic_apn");
|
||||
conf.Remove("apn");
|
||||
conf.Remove("apn_host");
|
||||
AnimeON = conf.ToObject<OnlinesSettings>();
|
||||
if (hasApn)
|
||||
ApnHelper.ApplyInitConf(apnEnabled, apnHost, AnimeON);
|
||||
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
|
||||
if (hasApn && apnEnabled)
|
||||
ApnHostProvided = ApnHelper.IsEnabled(AnimeON);
|
||||
if (ApnHostProvided)
|
||||
{
|
||||
AnimeON.streamproxy = false;
|
||||
}
|
||||
@ -74,7 +84,45 @@ namespace AnimeON
|
||||
}
|
||||
|
||||
// Виводити "уточнити пошук"
|
||||
AppInit.conf.online.with_search.Add("animeon");
|
||||
RegisterWithSearch("animeon");
|
||||
}
|
||||
|
||||
private static void RegisterWithSearch(string plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(plugin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,47 +1,36 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Base;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AnimeON
|
||||
{
|
||||
public class OnlineApi
|
||||
public class OnlineApi : IModuleOnline
|
||||
{
|
||||
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||
HttpContext httpContext,
|
||||
IMemoryCache memoryCache,
|
||||
RequestModel requestInfo,
|
||||
string host,
|
||||
OnlineEventsModel args)
|
||||
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
|
||||
{
|
||||
long.TryParse(args.id, out long tmdbid);
|
||||
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||
}
|
||||
|
||||
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
private static List<ModuleOnlineItem> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
{
|
||||
var online = new List<(string name, string url, string plugin, int index)>();
|
||||
var online = new List<ModuleOnlineItem>();
|
||||
|
||||
var init = ModInit.AnimeON;
|
||||
|
||||
// Визначаємо isAnime згідно стандарту Lampac (Deepwiki):
|
||||
// isanime = true якщо original_language == "ja" або "zh"
|
||||
bool hasLang = !string.IsNullOrEmpty(original_language);
|
||||
bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
|
||||
|
||||
// AnimeON — аніме-провайдер. Додаємо його:
|
||||
// - при загальному пошуку (serial == -1), або
|
||||
// - якщо контент визначений як аніме (isanime), або
|
||||
// - якщо мова невідома (відсутній original_language)
|
||||
if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang))
|
||||
{
|
||||
string url = init.overridehost;
|
||||
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||
url = $"{host}/animeon";
|
||||
if (UpdateService.IsDisconnected())
|
||||
init.overridehost = null;
|
||||
|
||||
online.Add((init.displayname, url, "animeon", init.displayindex));
|
||||
online.Add(new ModuleOnlineItem(init, "animeon"));
|
||||
}
|
||||
|
||||
return online;
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "AnimeON.ModInit",
|
||||
"online": "AnimeON.OnlineApi"
|
||||
"enable": true
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -20,13 +20,15 @@ namespace Bamboo
|
||||
private readonly IHybridCache _hybridCache;
|
||||
private readonly Action<string> _onLog;
|
||||
private readonly ProxyManager _proxyManager;
|
||||
private readonly HttpHydra _httpHydra;
|
||||
|
||||
public BambooInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||
public BambooInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
|
||||
{
|
||||
_init = init;
|
||||
_hybridCache = hybridCache;
|
||||
_onLog = onLog;
|
||||
_proxyManager = proxyManager;
|
||||
_httpHydra = httpHydra;
|
||||
}
|
||||
|
||||
public async Task<List<SearchResult>> Search(string title, string original_title)
|
||||
@ -50,7 +52,7 @@ namespace Bamboo
|
||||
};
|
||||
|
||||
_onLog?.Invoke($"Bamboo search: {searchUrl}");
|
||||
string html = await Http.Get(_init.cors(searchUrl), headers: headers, proxy: _proxyManager.Get());
|
||||
string html = await HttpGet(searchUrl, headers);
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return null;
|
||||
|
||||
@ -109,7 +111,7 @@ namespace Bamboo
|
||||
};
|
||||
|
||||
_onLog?.Invoke($"Bamboo series page: {href}");
|
||||
string html = await Http.Get(_init.cors(href), headers: headers, proxy: _proxyManager.Get());
|
||||
string html = await HttpGet(href, headers);
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return null;
|
||||
|
||||
@ -183,7 +185,7 @@ namespace Bamboo
|
||||
};
|
||||
|
||||
_onLog?.Invoke($"Bamboo movie page: {href}");
|
||||
string html = await Http.Get(_init.cors(href), headers: headers, proxy: _proxyManager.Get());
|
||||
string html = await HttpGet(href, headers);
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return null;
|
||||
|
||||
@ -311,12 +313,20 @@ namespace Bamboo
|
||||
return HtmlEntity.DeEntitize(value).Trim();
|
||||
}
|
||||
|
||||
private Task<string> HttpGet(string url, List<HeadersModel> headers)
|
||||
{
|
||||
if (_httpHydra != null)
|
||||
return _httpHydra.Get(url, newheaders: headers);
|
||||
|
||||
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||
}
|
||||
|
||||
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
|
||||
{
|
||||
if (init != null && init.rhub && rhub != -1)
|
||||
return TimeSpan.FromMinutes(rhub);
|
||||
|
||||
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
|
||||
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
|
||||
if (ctime > multiaccess)
|
||||
ctime = multiaccess;
|
||||
|
||||
|
||||
@ -23,27 +23,27 @@ namespace Bamboo.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("bamboo")]
|
||||
[Route("lite/bamboo")]
|
||||
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, bool rjson = false, string href = null, bool checksearch = false)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.Bamboo);
|
||||
var init = loadKit(ModInit.Bamboo);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
var invoke = new BambooInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
var invoke = new BambooInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
|
||||
if (checksearch)
|
||||
{
|
||||
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||
return OnError("bamboo", proxyManager);
|
||||
if (!IsCheckOnlineSearchEnabled())
|
||||
return OnError("bamboo", refresh_proxy: true);
|
||||
|
||||
var searchResults = await invoke.Search(title, original_title);
|
||||
if (searchResults != null && searchResults.Count > 0)
|
||||
return Content("data-json=", "text/plain; charset=utf-8");
|
||||
|
||||
return OnError("bamboo", proxyManager);
|
||||
return OnError("bamboo", refresh_proxy: true);
|
||||
}
|
||||
|
||||
string itemUrl = href;
|
||||
@ -51,14 +51,14 @@ namespace Bamboo.Controllers
|
||||
{
|
||||
var searchResults = await invoke.Search(title, original_title);
|
||||
if (searchResults == null || searchResults.Count == 0)
|
||||
return OnError("bamboo", proxyManager);
|
||||
return OnError("bamboo", refresh_proxy: true);
|
||||
|
||||
if (searchResults.Count > 1)
|
||||
{
|
||||
var similar_tpl = new SimilarTpl(searchResults.Count);
|
||||
foreach (var res in searchResults)
|
||||
{
|
||||
string link = $"{host}/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}";
|
||||
string link = $"{host}/lite/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}";
|
||||
similar_tpl.Append(res.Title, string.Empty, string.Empty, link, res.Poster);
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ namespace Bamboo.Controllers
|
||||
{
|
||||
var series = await invoke.GetSeriesEpisodes(itemUrl);
|
||||
if (series == null || (series.Sub.Count == 0 && series.Dub.Count == 0))
|
||||
return OnError("bamboo", proxyManager);
|
||||
return OnError("bamboo", refresh_proxy: true);
|
||||
|
||||
var voice_tpl = new VoiceTpl();
|
||||
var episode_tpl = new EpisodeTpl();
|
||||
@ -88,13 +88,13 @@ namespace Bamboo.Controllers
|
||||
|
||||
foreach (var voice in availableVoices)
|
||||
{
|
||||
string voiceLink = $"{host}/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&t={voice.key}&href={HttpUtility.UrlEncode(itemUrl)}";
|
||||
string voiceLink = $"{host}/lite/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&t={voice.key}&href={HttpUtility.UrlEncode(itemUrl)}";
|
||||
voice_tpl.Append(voice.name, voice.key == t, voiceLink);
|
||||
}
|
||||
|
||||
var selected = availableVoices.FirstOrDefault(v => v.key == t);
|
||||
if (selected.episodes == null || selected.episodes.Count == 0)
|
||||
return OnError("bamboo", proxyManager);
|
||||
return OnError("bamboo", refresh_proxy: true);
|
||||
|
||||
int index = 1;
|
||||
foreach (var ep in selected.episodes.OrderBy(e => e.Episode ?? int.MaxValue))
|
||||
@ -116,7 +116,7 @@ namespace Bamboo.Controllers
|
||||
{
|
||||
var streams = await invoke.GetMovieStreams(itemUrl);
|
||||
if (streams == null || streams.Count == 0)
|
||||
return OnError("bamboo", proxyManager);
|
||||
return OnError("bamboo", refresh_proxy: true);
|
||||
|
||||
var movie_tpl = new MovieTpl(title, original_title);
|
||||
for (int i = 0; i < streams.Count; i++)
|
||||
@ -166,5 +166,38 @@ namespace Bamboo.Controllers
|
||||
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private static bool IsCheckOnlineSearchEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (checkProp?.GetValue(conf) is bool enabled)
|
||||
return enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void OnLog(string message)
|
||||
{
|
||||
System.Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
Bamboo/GlobalUsings.cs
Normal file
4
Bamboo/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
global using Shared.Services;
|
||||
global using Shared.Services.Hybrid;
|
||||
global using Shared.Models.Base;
|
||||
global using AppInit = Shared.CoreInit;
|
||||
@ -3,6 +3,7 @@ using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models.Online.Settings;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
@ -22,9 +23,9 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Bamboo
|
||||
{
|
||||
public class ModInit
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 3.6;
|
||||
public static double Version => 4.0;
|
||||
|
||||
public static OnlinesSettings Bamboo;
|
||||
public static bool ApnHostProvided;
|
||||
@ -38,7 +39,7 @@ namespace Bamboo
|
||||
/// <summary>
|
||||
/// модуль загружен
|
||||
/// </summary>
|
||||
public static void loaded(InitspaceModel initspace)
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
|
||||
|
||||
@ -54,7 +55,7 @@ namespace Bamboo
|
||||
list = new string[] { "socks5://ip:port" }
|
||||
}
|
||||
};
|
||||
var conf = ModuleInvoke.Conf("Bamboo", Bamboo);
|
||||
var conf = ModuleInvoke.Init("Bamboo", JObject.FromObject(Bamboo));
|
||||
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||
conf.Remove("apn");
|
||||
conf.Remove("apn_host");
|
||||
@ -73,7 +74,45 @@ namespace Bamboo
|
||||
}
|
||||
|
||||
// Виводити "уточнити пошук"
|
||||
AppInit.conf.online.with_search.Add("bamboo");
|
||||
RegisterWithSearch("bamboo");
|
||||
}
|
||||
|
||||
private static void RegisterWithSearch(string plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(plugin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,28 +1,24 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Base;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bamboo
|
||||
{
|
||||
public class OnlineApi
|
||||
public class OnlineApi : IModuleOnline
|
||||
{
|
||||
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||
HttpContext httpContext,
|
||||
IMemoryCache memoryCache,
|
||||
RequestModel requestInfo,
|
||||
string host,
|
||||
OnlineEventsModel args)
|
||||
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
|
||||
{
|
||||
long.TryParse(args.id, out long tmdbid);
|
||||
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||
}
|
||||
|
||||
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
private static List<ModuleOnlineItem> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
{
|
||||
var online = new List<(string name, string url, string plugin, int index)>();
|
||||
var online = new List<ModuleOnlineItem>();
|
||||
|
||||
var init = ModInit.Bamboo;
|
||||
if (init.enable && !init.rip)
|
||||
@ -34,11 +30,10 @@ namespace Bamboo
|
||||
return online;
|
||||
}
|
||||
|
||||
string url = init.overridehost;
|
||||
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||
url = $"{host}/bamboo";
|
||||
if (UpdateService.IsDisconnected())
|
||||
init.overridehost = null;
|
||||
|
||||
online.Add((init.displayname, url, "bamboo", init.displayindex));
|
||||
online.Add(new ModuleOnlineItem(init, "bamboo"));
|
||||
}
|
||||
|
||||
return online;
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "Bamboo.ModInit",
|
||||
"online": "Bamboo.OnlineApi"
|
||||
"enable": true
|
||||
}
|
||||
@ -1,10 +1,9 @@
|
||||
using JackTor.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Online.PiTor;
|
||||
using Shared.Models.Online.Settings;
|
||||
using Shared.Services.Utilities;
|
||||
using Shared.Models.Templates;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -27,7 +26,7 @@ namespace JackTor.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("jacktor")]
|
||||
[Route("lite/jacktor")]
|
||||
async public Task<ActionResult> Index(
|
||||
long id,
|
||||
string imdb_id,
|
||||
@ -46,7 +45,7 @@ namespace JackTor.Controllers
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.Settings);
|
||||
var init = loadKit(ModInit.Settings);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
@ -57,14 +56,14 @@ namespace JackTor.Controllers
|
||||
|
||||
if (checksearch)
|
||||
{
|
||||
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||
return OnError("jacktor", proxyManager);
|
||||
if (!IsCheckOnlineSearchEnabled())
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
|
||||
var check = await invoke.Search(title, original_title, year, serial, original_language);
|
||||
if (check.Count > 0)
|
||||
return Content("data-json=", "text/plain; charset=utf-8");
|
||||
|
||||
return OnError("jacktor", proxyManager);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var torrents = await invoke.Search(title, original_title, year, serial, original_language);
|
||||
@ -98,7 +97,7 @@ namespace JackTor.Controllers
|
||||
{
|
||||
seasonTpl.Append(
|
||||
$"{season} сезон",
|
||||
$"{host}/jacktor?rjson={rjson}&title={enTitle}&original_title={enOriginal}&year={year}&original_language={original_language}&serial=1&s={season}",
|
||||
$"{host}/lite/jacktor?rjson={rjson}&title={enTitle}&original_title={enOriginal}&year={year}&original_language={original_language}&serial=1&s={season}",
|
||||
season);
|
||||
}
|
||||
|
||||
@ -127,7 +126,7 @@ namespace JackTor.Controllers
|
||||
: $"{seasonLabel} • {torrent.Voice}";
|
||||
|
||||
string qualityInfo = $"{torrent.Tracker} / {torrent.QualityLabel} / {torrent.MediaInfo} / ↑{torrent.Seeders}";
|
||||
string releaseLink = accsArgs($"{host}/jacktor/serial/{torrent.Rid}?rjson={rjson}&title={enTitle}&original_title={enOriginal}&s={targetSeason}");
|
||||
string releaseLink = accsArgs($"{host}/lite/jacktor/serial/{torrent.Rid}?rjson={rjson}&title={enTitle}&original_title={enOriginal}&s={targetSeason}");
|
||||
|
||||
similarTpl.Append(releaseName, null, qualityInfo, releaseLink);
|
||||
}
|
||||
@ -147,7 +146,7 @@ namespace JackTor.Controllers
|
||||
: torrent.Voice;
|
||||
|
||||
string voiceName = $"{torrent.QualityLabel} / {torrent.MediaInfo} / ↑{torrent.Seeders}";
|
||||
string streamLink = accsArgs($"{host}/jacktor/s{torrent.Rid}");
|
||||
string streamLink = accsArgs($"{host}/lite/jacktor/s{torrent.Rid}");
|
||||
|
||||
movieTpl.Append(
|
||||
voice,
|
||||
@ -163,10 +162,10 @@ namespace JackTor.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("jacktor/serial/{rid}")]
|
||||
[Route("lite/jacktor/serial/{rid}")]
|
||||
async public ValueTask<ActionResult> Serial(string rid, string account_email, string title, string original_title, int s = 1, bool rjson = false)
|
||||
{
|
||||
var init = await loadKit(ModInit.Settings);
|
||||
var init = loadKit(ModInit.Settings);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
@ -175,7 +174,7 @@ namespace JackTor.Controllers
|
||||
|
||||
var invoke = new JackTorInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
if (!invoke.TryGetSource(rid, out JackTorSourceCache source))
|
||||
return OnError("jacktor", proxyManager);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
|
||||
string memKey = $"jacktor:serial:{rid}";
|
||||
|
||||
@ -185,49 +184,49 @@ namespace JackTor.Controllers
|
||||
{
|
||||
var ts = ResolveProbeTorrentServer(init, account_email);
|
||||
if (string.IsNullOrWhiteSpace(ts.host))
|
||||
return OnError("jacktor", proxyManager);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
|
||||
string hashResponse = await Http.Post(
|
||||
string hashResponse = await httpHydra.Post(
|
||||
$"{ts.host}/torrents",
|
||||
BuildAddPayload(source.SourceUri),
|
||||
timeoutSeconds: 8,
|
||||
headers: ts.headers);
|
||||
statusCodeOK: false,
|
||||
newheaders: ts.headers);
|
||||
|
||||
string hash = ExtractHash(hashResponse);
|
||||
if (string.IsNullOrWhiteSpace(hash))
|
||||
return OnError("jacktor", proxyManager);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
|
||||
Stat stat = null;
|
||||
DateTime deadline = DateTime.Now.AddSeconds(20);
|
||||
|
||||
while (true)
|
||||
{
|
||||
stat = await Http.Post<Stat>(
|
||||
stat = await httpHydra.Post<Stat>(
|
||||
$"{ts.host}/torrents",
|
||||
BuildGetPayload(hash),
|
||||
timeoutSeconds: 3,
|
||||
headers: ts.headers);
|
||||
statusCodeOK: false,
|
||||
newheaders: ts.headers);
|
||||
|
||||
if (stat?.file_stats != null && stat.file_stats.Length > 0)
|
||||
break;
|
||||
|
||||
if (DateTime.Now > deadline)
|
||||
{
|
||||
_ = Http.Post($"{ts.host}/torrents", BuildRemovePayload(hash), headers: ts.headers);
|
||||
return OnError("jacktor", proxyManager);
|
||||
_ = httpHydra.Post($"{ts.host}/torrents", BuildRemovePayload(hash), statusCodeOK: false, newheaders: ts.headers);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
}
|
||||
|
||||
await Task.Delay(250);
|
||||
}
|
||||
|
||||
_ = Http.Post($"{ts.host}/torrents", BuildRemovePayload(hash), headers: ts.headers);
|
||||
_ = httpHydra.Post($"{ts.host}/torrents", BuildRemovePayload(hash), statusCodeOK: false, newheaders: ts.headers);
|
||||
|
||||
fileStats = stat.file_stats;
|
||||
hybridCache.Set(memKey, fileStats, DateTime.Now.AddHours(36));
|
||||
}
|
||||
|
||||
if (fileStats == null || fileStats.Length == 0)
|
||||
return OnError("jacktor", proxyManager);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
|
||||
var episodeTpl = new EpisodeTpl();
|
||||
int appended = 0;
|
||||
@ -242,13 +241,13 @@ namespace JackTor.Controllers
|
||||
title ?? original_title,
|
||||
s.ToString(),
|
||||
file.Id.ToString(),
|
||||
accsArgs($"{host}/jacktor/s{rid}?tsid={file.Id}"));
|
||||
accsArgs($"{host}/lite/jacktor/s{rid}?tsid={file.Id}"));
|
||||
|
||||
appended++;
|
||||
}
|
||||
|
||||
if (appended == 0)
|
||||
return OnError("jacktor", proxyManager);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
|
||||
return rjson
|
||||
? Content(episodeTpl.ToJson(), "application/json; charset=utf-8")
|
||||
@ -257,10 +256,10 @@ namespace JackTor.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("jacktor/s{rid}")]
|
||||
[Route("lite/jacktor/s{rid}")]
|
||||
async public ValueTask<ActionResult> Stream(string rid, int tsid = -1, string account_email = null)
|
||||
{
|
||||
var init = await loadKit(ModInit.Settings);
|
||||
var init = loadKit(ModInit.Settings);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
@ -269,7 +268,7 @@ namespace JackTor.Controllers
|
||||
|
||||
var invoke = new JackTorInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
if (!invoke.TryGetSource(rid, out JackTorSourceCache source))
|
||||
return OnError("jacktor", proxyManager);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
|
||||
int index = tsid != -1 ? tsid : 1;
|
||||
string country = requestInfo.Country;
|
||||
@ -284,15 +283,15 @@ namespace JackTor.Controllers
|
||||
var headers = HeadersModel.Init("Authorization", $"Basic {CrypTo.Base64($"{login}:{passwd}")}");
|
||||
headers = HeadersModel.Join(headers, addheaders);
|
||||
|
||||
string response = await Http.Post(
|
||||
string response = await httpHydra.Post(
|
||||
$"{tsHost}/torrents",
|
||||
BuildAddPayload(source.SourceUri),
|
||||
timeoutSeconds: 5,
|
||||
headers: headers);
|
||||
statusCodeOK: false,
|
||||
newheaders: headers);
|
||||
|
||||
hash = ExtractHash(response);
|
||||
if (string.IsNullOrWhiteSpace(hash))
|
||||
return OnError("jacktor", proxyManager);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
|
||||
hybridCache.Set(memKey, hash, DateTime.Now.AddMinutes(1));
|
||||
}
|
||||
@ -329,7 +328,7 @@ namespace JackTor.Controllers
|
||||
}
|
||||
|
||||
if (servers.Count == 0)
|
||||
return OnError("jacktor", proxyManager);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
|
||||
ts = servers[Random.Shared.Next(0, servers.Count)];
|
||||
hybridCache.Set(tsKey, ts, DateTime.Now.AddHours(4));
|
||||
@ -342,7 +341,7 @@ namespace JackTor.Controllers
|
||||
if (init.base_auth != null && init.base_auth.enable)
|
||||
{
|
||||
if (init.torrs == null || init.torrs.Length == 0)
|
||||
return OnError("jacktor", proxyManager);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
|
||||
string tsKey = $"jacktor:ts3:{rid}:{requestInfo.IP}";
|
||||
if (!hybridCache.TryGetValue(tsKey, out string tsHost))
|
||||
@ -355,7 +354,7 @@ namespace JackTor.Controllers
|
||||
}
|
||||
|
||||
if (init.torrs == null || init.torrs.Length == 0)
|
||||
return OnError("jacktor", proxyManager);
|
||||
return OnError("jacktor", refresh_proxy: true);
|
||||
|
||||
string key = $"jacktor:ts4:{rid}:{requestInfo.IP}";
|
||||
if (!hybridCache.TryGetValue(key, out string torrentHost))
|
||||
@ -474,5 +473,38 @@ namespace JackTor.Controllers
|
||||
|
||||
return Regex.Replace(url, "(apikey=)[^&]+", "$1***", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsCheckOnlineSearchEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (checkProp?.GetValue(conf) is bool enabled)
|
||||
return enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void OnLog(string message)
|
||||
{
|
||||
System.Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
JackTor/GlobalUsings.cs
Normal file
4
JackTor/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
global using Shared.Services;
|
||||
global using Shared.Services.Hybrid;
|
||||
global using Shared.Models.Base;
|
||||
global using AppInit = Shared.CoreInit;
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using JackTor.Models;
|
||||
using Shared.Engine;
|
||||
using Shared.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@ -2,8 +2,8 @@ using JackTor.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
@ -15,9 +15,9 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace JackTor
|
||||
{
|
||||
public class ModInit
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 1.0;
|
||||
public static double Version => 2.0;
|
||||
|
||||
public static JackTorSettings JackTor;
|
||||
|
||||
@ -30,7 +30,7 @@ namespace JackTor
|
||||
/// <summary>
|
||||
/// Модуль завантажено.
|
||||
/// </summary>
|
||||
public static void loaded(InitspaceModel initspace)
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
JackTor = new JackTorSettings("JackTor", "http://127.0.0.1:9117", streamproxy: false, useproxy: false)
|
||||
{
|
||||
@ -66,7 +66,7 @@ namespace JackTor
|
||||
}
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Conf("JackTor", JackTor) ?? JObject.FromObject(JackTor);
|
||||
var conf = ModuleInvoke.Init("JackTor", JObject.FromObject(JackTor)) ?? JObject.FromObject(JackTor);
|
||||
JackTor = conf.ToObject<JackTorSettings>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(JackTor.jackett))
|
||||
@ -76,7 +76,45 @@ namespace JackTor
|
||||
JackTor.host = JackTor.jackett;
|
||||
|
||||
// Показувати «уточнити пошук».
|
||||
AppInit.conf.online.with_search.Add("jacktor");
|
||||
RegisterWithSearch("jacktor");
|
||||
}
|
||||
|
||||
private static void RegisterWithSearch(string plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(plugin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -197,4 +197,18 @@ namespace JackTor.Models
|
||||
|
||||
public int[] Seasons { get; set; }
|
||||
}
|
||||
|
||||
public class FileStat
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Path { get; set; }
|
||||
|
||||
public long Length { get; set; }
|
||||
}
|
||||
|
||||
public class Stat
|
||||
{
|
||||
public FileStat[] file_stats { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,46 +2,31 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JackTor
|
||||
{
|
||||
public class OnlineApi
|
||||
public class OnlineApi : IModuleOnline
|
||||
{
|
||||
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||
HttpContext httpContext,
|
||||
IMemoryCache memoryCache,
|
||||
RequestModel requestInfo,
|
||||
string host,
|
||||
OnlineEventsModel args)
|
||||
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
|
||||
{
|
||||
long.TryParse(args.id, out long tmdbid);
|
||||
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||
}
|
||||
|
||||
public static List<(string name, string url, string plugin, int index)> Events(
|
||||
string host,
|
||||
long id,
|
||||
string imdb_id,
|
||||
long kinopoisk_id,
|
||||
string title,
|
||||
string original_title,
|
||||
string original_language,
|
||||
int year,
|
||||
string source,
|
||||
int serial,
|
||||
string account_email)
|
||||
private static List<ModuleOnlineItem> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
{
|
||||
var online = new List<(string name, string url, string plugin, int index)>();
|
||||
var online = new List<ModuleOnlineItem>();
|
||||
|
||||
var init = ModInit.JackTor;
|
||||
if (init.enable && !init.rip)
|
||||
{
|
||||
string url = init.overridehost;
|
||||
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||
url = $"{host}/jacktor";
|
||||
if (UpdateService.IsDisconnected())
|
||||
init.overridehost = null;
|
||||
|
||||
online.Add((init.displayname, url, "jacktor", init.displayindex));
|
||||
online.Add(new ModuleOnlineItem(init, "jacktor"));
|
||||
}
|
||||
|
||||
return online;
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "JackTor.ModInit",
|
||||
"online": "JackTor.OnlineApi"
|
||||
"enable": true
|
||||
}
|
||||
|
||||
@ -25,6 +25,23 @@ namespace Shared.Engine
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string TryGetMagicAshdiHost(JObject conf)
|
||||
{
|
||||
if (conf == null || !conf.TryGetValue("magic_apn", out var magicToken) || magicToken == null)
|
||||
return null;
|
||||
|
||||
if (magicToken.Type == JTokenType.Boolean)
|
||||
return magicToken.Value<bool>() ? DefaultHost : null;
|
||||
|
||||
if (magicToken.Type == JTokenType.String)
|
||||
return NormalizeHost(magicToken.Value<string>());
|
||||
|
||||
if (magicToken.Type != JTokenType.Object)
|
||||
return null;
|
||||
|
||||
return NormalizeHost(((JObject)magicToken).Value<string>("ashdi"));
|
||||
}
|
||||
|
||||
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
|
||||
{
|
||||
if (init == null)
|
||||
@ -37,8 +54,13 @@ namespace Shared.Engine
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
host = DefaultHost;
|
||||
host = NormalizeHost(host);
|
||||
if (host == null)
|
||||
{
|
||||
init.apnstream = false;
|
||||
init.apn = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (init.apn == null)
|
||||
init.apn = new ApnConf();
|
||||
@ -82,5 +104,13 @@ namespace Shared.Engine
|
||||
|
||||
return $"{host.TrimEnd('/')}/{url}";
|
||||
}
|
||||
|
||||
private static string NormalizeHost(string host)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
return null;
|
||||
|
||||
return host.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,27 +22,29 @@ namespace KlonFUN.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("klonfun")]
|
||||
[Route("lite/klonfun")]
|
||||
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, bool rjson = false, string href = null, bool checksearch = false)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.KlonFUN);
|
||||
var init = loadKit(ModInit.KlonFUN);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
var invoke = new KlonFUNInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
TryEnableMagicApn(init);
|
||||
|
||||
var invoke = new KlonFUNInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
|
||||
if (checksearch)
|
||||
{
|
||||
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||
return OnError("klonfun", proxyManager);
|
||||
if (!IsCheckOnlineSearchEnabled())
|
||||
return OnError("klonfun", refresh_proxy: true);
|
||||
|
||||
var checkResults = await invoke.Search(imdb_id, title, original_title);
|
||||
if (checkResults != null && checkResults.Count > 0)
|
||||
return Content("data-json=", "text/plain; charset=utf-8");
|
||||
|
||||
return OnError("klonfun", proxyManager);
|
||||
return OnError("klonfun", refresh_proxy: true);
|
||||
}
|
||||
|
||||
string itemUrl = href;
|
||||
@ -50,14 +52,14 @@ namespace KlonFUN.Controllers
|
||||
{
|
||||
var searchResults = await invoke.Search(imdb_id, title, original_title);
|
||||
if (searchResults == null || searchResults.Count == 0)
|
||||
return OnError("klonfun", proxyManager);
|
||||
return OnError("klonfun", refresh_proxy: true);
|
||||
|
||||
if (searchResults.Count > 1)
|
||||
{
|
||||
var similarTpl = new SimilarTpl(searchResults.Count);
|
||||
foreach (SearchResult result in searchResults)
|
||||
{
|
||||
string link = $"{host}/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(result.Url)}";
|
||||
string link = $"{host}/lite/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(result.Url)}";
|
||||
similarTpl.Append(result.Title, result.Year > 0 ? result.Year.ToString() : string.Empty, string.Empty, link, result.Poster);
|
||||
}
|
||||
|
||||
@ -73,7 +75,7 @@ namespace KlonFUN.Controllers
|
||||
if (item == null || string.IsNullOrWhiteSpace(item.PlayerUrl))
|
||||
{
|
||||
OnLog($"KlonFUN: не знайдено iframe-плеєр для {itemUrl}");
|
||||
return OnError("klonfun", proxyManager);
|
||||
return OnError("klonfun", refresh_proxy: true);
|
||||
}
|
||||
|
||||
string contentTitle = !string.IsNullOrWhiteSpace(title) ? title : item.Title;
|
||||
@ -87,7 +89,7 @@ namespace KlonFUN.Controllers
|
||||
{
|
||||
var serialStructure = await invoke.GetSerialStructure(item.PlayerUrl);
|
||||
if (serialStructure == null || serialStructure.Voices.Count == 0)
|
||||
return OnError("klonfun", proxyManager);
|
||||
return OnError("klonfun", refresh_proxy: true);
|
||||
|
||||
if (s == -1)
|
||||
{
|
||||
@ -118,12 +120,12 @@ namespace KlonFUN.Controllers
|
||||
}
|
||||
|
||||
if (seasons.Count == 0)
|
||||
return OnError("klonfun", proxyManager);
|
||||
return OnError("klonfun", refresh_proxy: true);
|
||||
|
||||
var seasonTpl = new SeasonTpl(seasons.Count);
|
||||
foreach (int seasonNumber in seasons)
|
||||
{
|
||||
string link = $"{host}/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}&href={HttpUtility.UrlEncode(itemUrl)}";
|
||||
string link = $"{host}/lite/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}&href={HttpUtility.UrlEncode(itemUrl)}";
|
||||
if (!string.IsNullOrWhiteSpace(t))
|
||||
link += $"&t={HttpUtility.UrlEncode(t)}";
|
||||
|
||||
@ -140,7 +142,7 @@ namespace KlonFUN.Controllers
|
||||
.ToList();
|
||||
|
||||
if (voicesForSeason.Count == 0)
|
||||
return OnError("klonfun", proxyManager);
|
||||
return OnError("klonfun", refresh_proxy: true);
|
||||
|
||||
var selectedVoiceForSeason = voicesForSeason
|
||||
.FirstOrDefault(v => !string.IsNullOrWhiteSpace(t) && v.Key.Equals(t, StringComparison.OrdinalIgnoreCase))
|
||||
@ -149,12 +151,12 @@ namespace KlonFUN.Controllers
|
||||
var voiceTpl = new VoiceTpl(voicesForSeason.Count);
|
||||
foreach (var voice in voicesForSeason)
|
||||
{
|
||||
string voiceLink = $"{host}/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.Key)}&href={HttpUtility.UrlEncode(itemUrl)}";
|
||||
string voiceLink = $"{host}/lite/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.Key)}&href={HttpUtility.UrlEncode(itemUrl)}";
|
||||
voiceTpl.Append(voice.DisplayName, voice.Key.Equals(selectedVoiceForSeason.Key, StringComparison.OrdinalIgnoreCase), voiceLink);
|
||||
}
|
||||
|
||||
if (!selectedVoiceForSeason.Seasons.TryGetValue(s, out List<SerialEpisode> episodes) || episodes.Count == 0)
|
||||
return OnError("klonfun", proxyManager);
|
||||
return OnError("klonfun", refresh_proxy: true);
|
||||
|
||||
var episodeTpl = new EpisodeTpl(episodes.Count);
|
||||
foreach (SerialEpisode episode in episodes.OrderBy(e => e.Number))
|
||||
@ -177,7 +179,7 @@ namespace KlonFUN.Controllers
|
||||
{
|
||||
var streams = await invoke.GetMovieStreams(item.PlayerUrl);
|
||||
if (streams == null || streams.Count == 0)
|
||||
return OnError("klonfun", proxyManager);
|
||||
return OnError("klonfun", refresh_proxy: true);
|
||||
|
||||
var movieTpl = new MovieTpl(contentTitle, contentOriginalTitle, streams.Count);
|
||||
for (int i = 0; i < streams.Count; i++)
|
||||
@ -217,6 +219,24 @@ namespace KlonFUN.Controllers
|
||||
return HostStreamProxy(init, link);
|
||||
}
|
||||
|
||||
private void TryEnableMagicApn(OnlinesSettings init)
|
||||
{
|
||||
if (init == null
|
||||
|| init.apn != null
|
||||
|| init.streamproxy
|
||||
|| string.IsNullOrWhiteSpace(ModInit.MagicApnAshdiHost))
|
||||
return;
|
||||
|
||||
string player = new RchClient(HttpContext, host, init, requestInfo).InfoConnected()?.player;
|
||||
bool useInnerPlayer = string.IsNullOrWhiteSpace(player)
|
||||
|| player.Equals("inner", StringComparison.OrdinalIgnoreCase);
|
||||
if (!useInnerPlayer)
|
||||
return;
|
||||
|
||||
ApnHelper.ApplyInitConf(true, ModInit.MagicApnAshdiHost, init);
|
||||
OnLog($"KlonFUN: увімкнено magic_apn для Ashdi (player={player ?? "unknown"}).");
|
||||
}
|
||||
|
||||
private static string StripLampacArgs(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
@ -232,5 +252,38 @@ namespace KlonFUN.Controllers
|
||||
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private static bool IsCheckOnlineSearchEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (checkProp?.GetValue(conf) is bool enabled)
|
||||
return enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void OnLog(string message)
|
||||
{
|
||||
System.Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
KlonFUN/GlobalUsings.cs
Normal file
4
KlonFUN/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
global using Shared.Services;
|
||||
global using Shared.Services.Hybrid;
|
||||
global using Shared.Models.Base;
|
||||
global using AppInit = Shared.CoreInit;
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -28,13 +28,15 @@ namespace KlonFUN
|
||||
private readonly IHybridCache _hybridCache;
|
||||
private readonly Action<string> _onLog;
|
||||
private readonly ProxyManager _proxyManager;
|
||||
private readonly HttpHydra _httpHydra;
|
||||
|
||||
public KlonFUNInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||
public KlonFUNInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
|
||||
{
|
||||
_init = init;
|
||||
_hybridCache = hybridCache;
|
||||
_onLog = onLog;
|
||||
_proxyManager = proxyManager;
|
||||
_httpHydra = httpHydra;
|
||||
}
|
||||
|
||||
public async Task<List<SearchResult>> Search(string imdbId, string title, string originalTitle)
|
||||
@ -108,7 +110,7 @@ namespace KlonFUN
|
||||
try
|
||||
{
|
||||
var headers = DefaultHeaders();
|
||||
string html = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||
string html = await HttpGet(url, headers);
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
return null;
|
||||
|
||||
@ -362,7 +364,7 @@ namespace KlonFUN
|
||||
var headers = DefaultHeaders();
|
||||
|
||||
string form = $"do=search&subaction=search&story={HttpUtility.UrlEncode(query)}";
|
||||
string html = await Http.Post(_init.cors(_init.host), form, headers: headers, proxy: _proxyManager.Get());
|
||||
string html = await HttpPost(_init.host, form, headers);
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
return null;
|
||||
|
||||
@ -465,7 +467,7 @@ namespace KlonFUN
|
||||
requestUrl = ApnHelper.WrapUrl(_init, playerUrl);
|
||||
|
||||
var headers = DefaultHeaders();
|
||||
return await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
|
||||
return await HttpGet(requestUrl, headers);
|
||||
}
|
||||
|
||||
private static JArray ParsePlayerArray(string html)
|
||||
@ -699,16 +701,28 @@ namespace KlonFUN
|
||||
return null;
|
||||
}
|
||||
|
||||
private Task<string> HttpGet(string url, List<HeadersModel> headers)
|
||||
{
|
||||
if (_httpHydra != null)
|
||||
return _httpHydra.Get(url, newheaders: headers);
|
||||
|
||||
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||
}
|
||||
|
||||
private Task<string> HttpPost(string url, string data, List<HeadersModel> headers)
|
||||
{
|
||||
if (_httpHydra != null)
|
||||
return _httpHydra.Post(url, data, newheaders: headers);
|
||||
|
||||
return Http.Post(_init.cors(url), data, headers: headers, proxy: _proxyManager.Get());
|
||||
}
|
||||
|
||||
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
|
||||
{
|
||||
if (init != null && init.rhub && rhub != -1)
|
||||
return TimeSpan.FromMinutes(rhub);
|
||||
|
||||
int ctime = AppInit.conf.mikrotik
|
||||
? mikrotik
|
||||
: AppInit.conf.multiaccess
|
||||
? init != null && init.cache_time > 0 ? init.cache_time : multiaccess
|
||||
: home;
|
||||
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
|
||||
|
||||
if (ctime > multiaccess)
|
||||
ctime = multiaccess;
|
||||
|
||||
@ -3,6 +3,7 @@ using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models.Online.Settings;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
@ -13,17 +14,19 @@ using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Shared.Models.Events;
|
||||
|
||||
namespace KlonFUN
|
||||
{
|
||||
public class ModInit
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 1.1;
|
||||
public static double Version => 2.0;
|
||||
|
||||
public static OnlinesSettings KlonFUN;
|
||||
public static ModuleConfig KlonFUN;
|
||||
public static bool ApnHostProvided;
|
||||
public static string MagicApnAshdiHost;
|
||||
|
||||
public static OnlinesSettings Settings
|
||||
public static ModuleConfig Settings
|
||||
{
|
||||
get => KlonFUN;
|
||||
set => KlonFUN = value;
|
||||
@ -32,9 +35,18 @@ namespace KlonFUN
|
||||
/// <summary>
|
||||
/// Модуль завантажено.
|
||||
/// </summary>
|
||||
public static void loaded(InitspaceModel initspace)
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
KlonFUN = new OnlinesSettings("KlonFUN", "https://klon.fun", streamproxy: false, useproxy: false)
|
||||
UpdateConfig();
|
||||
EventListener.UpdateInitFile += UpdateConfig;
|
||||
|
||||
// Додаємо підтримку "уточнити пошук".
|
||||
RegisterWithSearch("klonfun");
|
||||
}
|
||||
|
||||
private void UpdateConfig()
|
||||
{
|
||||
KlonFUN = new ModuleConfig("KlonFUN", "https://klon.fun", streamproxy: false, useproxy: false)
|
||||
{
|
||||
displayname = "KlonFUN",
|
||||
displayindex = 0,
|
||||
@ -47,16 +59,24 @@ namespace KlonFUN
|
||||
}
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Conf("KlonFUN", KlonFUN);
|
||||
var defaults = JObject.FromObject(KlonFUN);
|
||||
defaults["magic_apn"] = new JObject()
|
||||
{
|
||||
["ashdi"] = ApnHelper.DefaultHost
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Init("KlonFUN", defaults) ?? defaults;
|
||||
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||
MagicApnAshdiHost = ApnHelper.TryGetMagicAshdiHost(conf);
|
||||
conf.Remove("magic_apn");
|
||||
conf.Remove("apn");
|
||||
conf.Remove("apn_host");
|
||||
KlonFUN = conf.ToObject<OnlinesSettings>();
|
||||
KlonFUN = conf.ToObject<ModuleConfig>();
|
||||
if (hasApn)
|
||||
ApnHelper.ApplyInitConf(apnEnabled, apnHost, KlonFUN);
|
||||
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
|
||||
ApnHostProvided = ApnHelper.IsEnabled(KlonFUN);
|
||||
|
||||
if (hasApn && apnEnabled)
|
||||
if (ApnHostProvided)
|
||||
{
|
||||
KlonFUN.streamproxy = false;
|
||||
}
|
||||
@ -65,9 +85,45 @@ namespace KlonFUN
|
||||
KlonFUN.apnstream = false;
|
||||
KlonFUN.apn = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Додаємо підтримку "уточнити пошук".
|
||||
AppInit.conf.online.with_search.Add("klonfun");
|
||||
private static void RegisterWithSearch(string plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(plugin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
EventListener.UpdateInitFile -= UpdateConfig;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
KlonFUN/ModuleConfig.cs
Normal file
18
KlonFUN/ModuleConfig.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@ -1,37 +1,32 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Base;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KlonFUN
|
||||
{
|
||||
public class OnlineApi
|
||||
public class OnlineApi : IModuleOnline
|
||||
{
|
||||
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||
HttpContext httpContext,
|
||||
IMemoryCache memoryCache,
|
||||
RequestModel requestInfo,
|
||||
string host,
|
||||
OnlineEventsModel args)
|
||||
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
|
||||
{
|
||||
long.TryParse(args.id, out long tmdbid);
|
||||
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||
}
|
||||
|
||||
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
private static List<ModuleOnlineItem> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
{
|
||||
var online = new List<(string name, string url, string plugin, int index)>();
|
||||
var online = new List<ModuleOnlineItem>();
|
||||
|
||||
var init = ModInit.KlonFUN;
|
||||
if (init.enable && !init.rip)
|
||||
{
|
||||
string url = init.overridehost;
|
||||
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||
url = $"{host}/klonfun";
|
||||
if (UpdateService.IsDisconnected())
|
||||
init.overridehost = null;
|
||||
|
||||
online.Add((init.displayname, url, "klonfun", init.displayindex));
|
||||
online.Add(new ModuleOnlineItem(init, "klonfun"));
|
||||
}
|
||||
|
||||
return online;
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "KlonFUN.ModInit",
|
||||
"online": "KlonFUN.OnlineApi"
|
||||
"enable": true
|
||||
}
|
||||
|
||||
@ -23,20 +23,37 @@ namespace Shared.Engine
|
||||
if (apnToken.Type == JTokenType.Boolean)
|
||||
{
|
||||
enabled = apnToken.Value<bool>();
|
||||
host = conf.Value<string>("apn_host");
|
||||
host = NormalizeHost(conf.Value<string>("apn_host"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (apnToken.Type == JTokenType.String)
|
||||
{
|
||||
host = apnToken.Value<string>();
|
||||
enabled = !string.IsNullOrWhiteSpace(host);
|
||||
host = NormalizeHost(apnToken.Value<string>());
|
||||
enabled = host != null;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string TryGetMagicAshdiHost(JObject conf)
|
||||
{
|
||||
if (conf == null || !conf.TryGetValue("magic_apn", out var magicToken) || magicToken == null)
|
||||
return null;
|
||||
|
||||
if (magicToken.Type == JTokenType.Boolean)
|
||||
return magicToken.Value<bool>() ? DefaultHost : null;
|
||||
|
||||
if (magicToken.Type == JTokenType.String)
|
||||
return NormalizeHost(magicToken.Value<string>());
|
||||
|
||||
if (magicToken.Type != JTokenType.Object)
|
||||
return null;
|
||||
|
||||
return NormalizeHost(((JObject)magicToken).Value<string>("ashdi"));
|
||||
}
|
||||
|
||||
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
|
||||
{
|
||||
if (init == null)
|
||||
@ -49,8 +66,13 @@ namespace Shared.Engine
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
host = DefaultHost;
|
||||
host = NormalizeHost(host);
|
||||
if (host == null)
|
||||
{
|
||||
init.apnstream = false;
|
||||
init.apn = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (init.apn == null)
|
||||
init.apn = new ApnConf();
|
||||
@ -94,5 +116,13 @@ namespace Shared.Engine
|
||||
|
||||
return $"{host.TrimEnd('/')}/{url}";
|
||||
}
|
||||
|
||||
private static string NormalizeHost(string host)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
return null;
|
||||
|
||||
return host.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ using Makhno.Models;
|
||||
|
||||
namespace Makhno
|
||||
{
|
||||
[Route("makhno")]
|
||||
[Route("lite/makhno")]
|
||||
public class MakhnoController : BaseOnlineController
|
||||
{
|
||||
private readonly ProxyManager proxyManager;
|
||||
@ -28,7 +28,7 @@ namespace Makhno
|
||||
{
|
||||
if (checksearch)
|
||||
{
|
||||
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||
if (!IsCheckOnlineSearchEnabled())
|
||||
return OnError();
|
||||
|
||||
return Content("data-json=", "text/plain; charset=utf-8");
|
||||
@ -36,14 +36,15 @@ namespace Makhno
|
||||
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.Makhno);
|
||||
var init = loadKit(ModInit.Makhno);
|
||||
if (!init.enable)
|
||||
return OnError();
|
||||
TryEnableMagicApn(init);
|
||||
Initialization(init);
|
||||
|
||||
OnLog($"Makhno: {title} (serial={serial}, s={s}, season={season}, t={t})");
|
||||
|
||||
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
|
||||
var resolved = await ResolvePlaySource(imdb_id, serial, invoke);
|
||||
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
|
||||
@ -61,14 +62,15 @@ namespace Makhno
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.Makhno);
|
||||
var init = loadKit(ModInit.Makhno);
|
||||
if (!init.enable)
|
||||
return OnError();
|
||||
TryEnableMagicApn(init);
|
||||
Initialization(init);
|
||||
|
||||
OnLog($"Makhno Play: {title} (s={s}, season={season}, t={t}, episodeId={episodeId}) play={play}");
|
||||
|
||||
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
var resolved = await ResolvePlaySource(imdb_id, serial: 1, invoke);
|
||||
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
|
||||
return OnError();
|
||||
@ -119,14 +121,15 @@ namespace Makhno
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.Makhno);
|
||||
var init = loadKit(ModInit.Makhno);
|
||||
if (!init.enable)
|
||||
return OnError();
|
||||
TryEnableMagicApn(init);
|
||||
Initialization(init);
|
||||
|
||||
OnLog($"Makhno PlayMovie: {title} ({year}) play={play}");
|
||||
|
||||
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
var resolved = await ResolvePlaySource(imdb_id, serial: 0, invoke);
|
||||
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
|
||||
return OnError();
|
||||
@ -267,7 +270,7 @@ namespace Makhno
|
||||
|
||||
string voiceParam = seasonVoiceIndex.HasValue ? $"&t={seasonVoiceIndex.Value}" : string.Empty;
|
||||
string seasonName = seasonItem.HasValue ? seasonItem.Value.Season?.Title ?? $"Сезон {seasonNumber}" : $"Сезон {seasonNumber}";
|
||||
string link = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}{voiceParam}";
|
||||
string link = $"{host}/lite/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}{voiceParam}";
|
||||
season_tpl.Append(seasonName, link, seasonNumber.ToString());
|
||||
}
|
||||
|
||||
@ -337,7 +340,7 @@ namespace Makhno
|
||||
|
||||
string voiceParam = seasonVoiceIndexForTpl.HasValue ? $"&t={seasonVoiceIndexForTpl.Value}" : string.Empty;
|
||||
string seasonName = seasonItem.HasValue ? seasonItem.Value.Season?.Title ?? $"Сезон {seasonNumber}" : $"Сезон {seasonNumber}";
|
||||
string link = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}{voiceParam}";
|
||||
string link = $"{host}/lite/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={seasonNumber}{voiceParam}";
|
||||
seasonTplForVoice.Append(seasonName, link, seasonNumber.ToString());
|
||||
}
|
||||
|
||||
@ -354,11 +357,11 @@ namespace Makhno
|
||||
bool sameSeasonSet = seasonsForVoice.Select(s => s.Number).ToHashSet().SetEquals(selectedVoiceSeasonSet);
|
||||
if (hasRequestedSeason && sameSeasonSet)
|
||||
{
|
||||
voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={requestedSeason}&t={i}";
|
||||
voiceLink = $"{host}/lite/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season={requestedSeason}&t={i}";
|
||||
}
|
||||
else
|
||||
{
|
||||
voiceLink = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season=-1&t={i}";
|
||||
voiceLink = $"{host}/lite/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season=-1&t={i}";
|
||||
}
|
||||
|
||||
bool isActive = selectedVoice == i.ToString();
|
||||
@ -374,7 +377,7 @@ namespace Makhno
|
||||
bool hasRequestedSeason = seasonsForVoice.Any(s => s.Number == requestedSeason);
|
||||
if (!hasRequestedSeason)
|
||||
{
|
||||
string redirectUrl = $"{host}/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season=-1&t={voiceIndex}";
|
||||
string redirectUrl = $"{host}/lite/makhno?imdb_id={imdb_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&season=-1&t={voiceIndex}";
|
||||
return UpdateService.Validate(Redirect(redirectUrl));
|
||||
}
|
||||
|
||||
@ -512,10 +515,61 @@ namespace Makhno
|
||||
return HostStreamProxy(init, link);
|
||||
}
|
||||
|
||||
private void TryEnableMagicApn(OnlinesSettings init)
|
||||
{
|
||||
if (init == null
|
||||
|| init.apn != null
|
||||
|| init.streamproxy
|
||||
|| string.IsNullOrWhiteSpace(ModInit.MagicApnAshdiHost))
|
||||
return;
|
||||
|
||||
string player = new RchClient(HttpContext, host, init, requestInfo).InfoConnected()?.player;
|
||||
bool useInnerPlayer = string.IsNullOrWhiteSpace(player)
|
||||
|| player.Equals("inner", StringComparison.OrdinalIgnoreCase);
|
||||
if (!useInnerPlayer)
|
||||
return;
|
||||
|
||||
ApnHelper.ApplyInitConf(true, ModInit.MagicApnAshdiHost, init);
|
||||
OnLog($"Makhno: увімкнено magic_apn для Ashdi (player={player ?? "unknown"}).");
|
||||
}
|
||||
|
||||
private class ResolveResult
|
||||
{
|
||||
public string PlayUrl { get; set; }
|
||||
public bool IsSerial { get; set; }
|
||||
}
|
||||
|
||||
private static bool IsCheckOnlineSearchEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (checkProp?.GetValue(conf) is bool enabled)
|
||||
return enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void OnLog(string message)
|
||||
{
|
||||
System.Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
Makhno/GlobalUsings.cs
Normal file
4
Makhno/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
global using Shared.Services;
|
||||
global using Shared.Services.Hybrid;
|
||||
global using Shared.Models.Base;
|
||||
global using AppInit = Shared.CoreInit;
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -24,13 +24,15 @@ namespace Makhno
|
||||
private readonly IHybridCache _hybridCache;
|
||||
private readonly Action<string> _onLog;
|
||||
private readonly ProxyManager _proxyManager;
|
||||
private readonly HttpHydra _httpHydra;
|
||||
|
||||
public MakhnoInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||
public MakhnoInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
|
||||
{
|
||||
_init = init;
|
||||
_hybridCache = hybridCache;
|
||||
_onLog = onLog;
|
||||
_proxyManager = proxyManager;
|
||||
_httpHydra = httpHydra;
|
||||
}
|
||||
|
||||
public async Task<string> GetWormholePlay(string imdbId)
|
||||
@ -46,7 +48,7 @@ namespace Makhno
|
||||
new HeadersModel("User-Agent", Http.UserAgent)
|
||||
};
|
||||
|
||||
string response = await Http.Get(_init.cors(url), timeoutSeconds: 4, headers: headers, proxy: _proxyManager.Get());
|
||||
string response = await HttpGet(url, headers, timeoutSeconds: 4);
|
||||
if (string.IsNullOrWhiteSpace(response))
|
||||
return null;
|
||||
|
||||
@ -84,7 +86,7 @@ namespace Makhno
|
||||
|
||||
_onLog($"Makhno getting player data from: {requestUrl}");
|
||||
|
||||
var response = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
|
||||
var response = await HttpGet(requestUrl, headers);
|
||||
if (string.IsNullOrEmpty(response))
|
||||
return null;
|
||||
|
||||
@ -526,6 +528,14 @@ namespace Makhno
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private Task<string> HttpGet(string url, List<HeadersModel> headers, int timeoutSeconds = 15)
|
||||
{
|
||||
if (_httpHydra != null)
|
||||
return _httpHydra.Get(url, newheaders: headers);
|
||||
|
||||
return Http.Get(_init.cors(url), timeoutSeconds: timeoutSeconds, headers: headers, proxy: _proxyManager.Get());
|
||||
}
|
||||
|
||||
private class WormholeResponse
|
||||
{
|
||||
public string play { get; set; }
|
||||
|
||||
@ -4,6 +4,7 @@ using Shared.Engine;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Shared.Models.Online.Settings;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
@ -21,12 +22,13 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Makhno
|
||||
{
|
||||
public class ModInit
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 2.1;
|
||||
public static double Version => 3.0;
|
||||
|
||||
public static OnlinesSettings Makhno;
|
||||
public static bool ApnHostProvided;
|
||||
public static string MagicApnAshdiHost;
|
||||
|
||||
public static OnlinesSettings Settings
|
||||
{
|
||||
@ -37,7 +39,7 @@ namespace Makhno
|
||||
/// <summary>
|
||||
/// модуль загружен
|
||||
/// </summary>
|
||||
public static void loaded(InitspaceModel initspace)
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
Makhno = new OnlinesSettings("Makhno", "https://wh.lme.isroot.in", streamproxy: false, useproxy: false)
|
||||
{
|
||||
@ -51,8 +53,16 @@ namespace Makhno
|
||||
list = new string[] { "socks5://ip:port" }
|
||||
}
|
||||
};
|
||||
var conf = ModuleInvoke.Conf("Makhno", Makhno);
|
||||
var defaults = JObject.FromObject(Makhno);
|
||||
defaults["magic_apn"] = new JObject()
|
||||
{
|
||||
["ashdi"] = ApnHelper.DefaultHost
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Init("Makhno", defaults) ?? defaults;
|
||||
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||
MagicApnAshdiHost = ApnHelper.TryGetMagicAshdiHost(conf);
|
||||
conf.Remove("magic_apn");
|
||||
if (hasApn)
|
||||
{
|
||||
conf.Remove("apn");
|
||||
@ -61,8 +71,8 @@ namespace Makhno
|
||||
Makhno = conf.ToObject<OnlinesSettings>();
|
||||
if (hasApn)
|
||||
ApnHelper.ApplyInitConf(apnEnabled, apnHost, Makhno);
|
||||
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
|
||||
if (hasApn && apnEnabled)
|
||||
ApnHostProvided = ApnHelper.IsEnabled(Makhno);
|
||||
if (ApnHostProvided)
|
||||
{
|
||||
Makhno.streamproxy = false;
|
||||
}
|
||||
@ -73,7 +83,45 @@ namespace Makhno
|
||||
}
|
||||
|
||||
// Виводити "уточнити пошук"
|
||||
AppInit.conf.online.with_search.Add("makhno");
|
||||
RegisterWithSearch("makhno");
|
||||
}
|
||||
|
||||
private static void RegisterWithSearch(string plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(plugin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,37 +1,32 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Base;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Makhno
|
||||
{
|
||||
public class OnlineApi
|
||||
public class OnlineApi : IModuleOnline
|
||||
{
|
||||
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||
HttpContext httpContext,
|
||||
IMemoryCache memoryCache,
|
||||
RequestModel requestInfo,
|
||||
string host,
|
||||
OnlineEventsModel args)
|
||||
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
|
||||
{
|
||||
long.TryParse(args.id, out long tmdbid);
|
||||
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||
}
|
||||
|
||||
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
private static List<ModuleOnlineItem> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
{
|
||||
var online = new List<(string name, string url, string plugin, int index)>();
|
||||
var online = new List<ModuleOnlineItem>();
|
||||
|
||||
var init = ModInit.Makhno;
|
||||
if (init.enable && !init.rip)
|
||||
{
|
||||
string url = init.overridehost;
|
||||
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||
url = $"{host}/makhno";
|
||||
if (UpdateService.IsDisconnected())
|
||||
init.overridehost = null;
|
||||
|
||||
online.Add((init.displayname, url, "makhno", init.displayindex));
|
||||
online.Add(new ModuleOnlineItem(init, "makhno"));
|
||||
}
|
||||
|
||||
return online;
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "Makhno.ModInit",
|
||||
"online": "Makhno.OnlineApi"
|
||||
"enable": true
|
||||
}
|
||||
|
||||
@ -25,6 +25,23 @@ namespace Shared.Engine
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string TryGetMagicAshdiHost(JObject conf)
|
||||
{
|
||||
if (conf == null || !conf.TryGetValue("magic_apn", out var magicToken) || magicToken == null)
|
||||
return null;
|
||||
|
||||
if (magicToken.Type == JTokenType.Boolean)
|
||||
return magicToken.Value<bool>() ? DefaultHost : null;
|
||||
|
||||
if (magicToken.Type == JTokenType.String)
|
||||
return NormalizeHost(magicToken.Value<string>());
|
||||
|
||||
if (magicToken.Type != JTokenType.Object)
|
||||
return null;
|
||||
|
||||
return NormalizeHost(((JObject)magicToken).Value<string>("ashdi"));
|
||||
}
|
||||
|
||||
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
|
||||
{
|
||||
if (init == null)
|
||||
@ -37,8 +54,13 @@ namespace Shared.Engine
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
host = DefaultHost;
|
||||
host = NormalizeHost(host);
|
||||
if (host == null)
|
||||
{
|
||||
init.apnstream = false;
|
||||
init.apn = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (init.apn == null)
|
||||
init.apn = new ApnConf();
|
||||
@ -82,5 +104,13 @@ namespace Shared.Engine
|
||||
|
||||
return $"{host.TrimEnd('/')}/{url}";
|
||||
}
|
||||
|
||||
private static string NormalizeHost(string host)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
return null;
|
||||
|
||||
return host.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,48 +23,49 @@ namespace Mikai.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("mikai")]
|
||||
[Route("lite/mikai")]
|
||||
public async Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, bool rjson = false, bool checksearch = false)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.Mikai);
|
||||
var init = loadKit(ModInit.Mikai);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager);
|
||||
TryEnableMagicApn(init);
|
||||
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager, httpHydra);
|
||||
|
||||
if (checksearch)
|
||||
{
|
||||
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||
return OnError("mikai", _proxyManager);
|
||||
if (!IsCheckOnlineSearchEnabled())
|
||||
return OnError("mikai", refresh_proxy: true);
|
||||
|
||||
var checkResults = await invoke.Search(title, original_title, year);
|
||||
if (checkResults != null && checkResults.Count > 0)
|
||||
return Content("data-json=", "text/plain; charset=utf-8");
|
||||
|
||||
return OnError("mikai", _proxyManager);
|
||||
return OnError("mikai", refresh_proxy: true);
|
||||
}
|
||||
|
||||
OnLog($"Mikai Index: title={title}, original_title={original_title}, serial={serial}, s={s}, t={t}, year={year}");
|
||||
|
||||
var searchResults = await invoke.Search(title, original_title, year);
|
||||
if (searchResults == null || searchResults.Count == 0)
|
||||
return OnError("mikai", _proxyManager);
|
||||
return OnError("mikai", refresh_proxy: true);
|
||||
|
||||
var selected = searchResults.FirstOrDefault();
|
||||
if (selected == null)
|
||||
return OnError("mikai", _proxyManager);
|
||||
return OnError("mikai", refresh_proxy: true);
|
||||
|
||||
var details = await invoke.GetDetails(selected.Id);
|
||||
if (details == null || details.Players == null || details.Players.Count == 0)
|
||||
return OnError("mikai", _proxyManager);
|
||||
return OnError("mikai", refresh_proxy: true);
|
||||
|
||||
bool isSerial = serial == 1 || (serial == -1 && !string.Equals(details.Format, "movie", StringComparison.OrdinalIgnoreCase));
|
||||
var seasonDetails = await CollectSeasonDetails(details, invoke);
|
||||
var voices = BuildVoices(seasonDetails);
|
||||
if (voices.Count == 0)
|
||||
return OnError("mikai", _proxyManager);
|
||||
return OnError("mikai", refresh_proxy: true);
|
||||
|
||||
string displayTitle = title ?? details.Details?.Names?.Name ?? original_title;
|
||||
|
||||
@ -81,14 +82,14 @@ namespace Mikai.Controllers
|
||||
.ToList();
|
||||
|
||||
if (seasonNumbers.Count == 0)
|
||||
return OnError("mikai", _proxyManager);
|
||||
return OnError("mikai", refresh_proxy: true);
|
||||
|
||||
if (s == -1)
|
||||
{
|
||||
var seasonTpl = new SeasonTpl(seasonNumbers.Count);
|
||||
foreach (var seasonNumber in seasonNumbers)
|
||||
{
|
||||
string link = $"{host}/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}";
|
||||
string link = $"{host}/lite/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}";
|
||||
if (restrictByVoice)
|
||||
link += $"&t={HttpUtility.UrlEncode(t)}";
|
||||
seasonTpl.Append($"{seasonNumber}", link, seasonNumber.ToString());
|
||||
@ -104,7 +105,7 @@ namespace Mikai.Controllers
|
||||
.ToList();
|
||||
|
||||
if (!voicesForSeason.Any())
|
||||
return OnError("mikai", _proxyManager);
|
||||
return OnError("mikai", refresh_proxy: true);
|
||||
|
||||
if (string.IsNullOrEmpty(t))
|
||||
t = voicesForSeason[0].Key;
|
||||
@ -118,7 +119,7 @@ namespace Mikai.Controllers
|
||||
{
|
||||
var targetSeasonSet = GetSeasonSet(voice.Value);
|
||||
bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet);
|
||||
string voiceLink = $"{host}/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1";
|
||||
string voiceLink = $"{host}/lite/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1";
|
||||
if (sameSeasonSet)
|
||||
voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
|
||||
else
|
||||
@ -128,7 +129,7 @@ namespace Mikai.Controllers
|
||||
|
||||
if (!voices.ContainsKey(t) || !voices[t].Seasons.ContainsKey(s))
|
||||
{
|
||||
string redirectUrl = $"{host}/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s=-1&t={HttpUtility.UrlEncode(t)}";
|
||||
string redirectUrl = $"{host}/lite/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s=-1&t={HttpUtility.UrlEncode(t)}";
|
||||
return Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
@ -143,7 +144,7 @@ namespace Mikai.Controllers
|
||||
|
||||
if (NeedsResolve(voices[t].ProviderName, streamLink))
|
||||
{
|
||||
string callUrl = $"{host}/mikai/play?url={HttpUtility.UrlEncode(streamLink)}&title={HttpUtility.UrlEncode(displayTitle)}&serial=1";
|
||||
string callUrl = $"{host}/lite/mikai/play?url={HttpUtility.UrlEncode(streamLink)}&title={HttpUtility.UrlEncode(displayTitle)}&serial=1";
|
||||
episodeTpl.Append(episodeName, displayTitle, s.ToString(), ep.Number.ToString(), accsArgs(callUrl), "call");
|
||||
}
|
||||
else
|
||||
@ -177,14 +178,14 @@ namespace Mikai.Controllers
|
||||
foreach (var ashdiStream in ashdiStreams)
|
||||
{
|
||||
string optionName = $"{voice.DisplayName} {ashdiStream.title}";
|
||||
string ashdiCallUrl = $"{host}/mikai/play?url={HttpUtility.UrlEncode(ashdiStream.link)}&title={HttpUtility.UrlEncode(displayTitle)}";
|
||||
string ashdiCallUrl = $"{host}/lite/mikai/play?url={HttpUtility.UrlEncode(ashdiStream.link)}&title={HttpUtility.UrlEncode(displayTitle)}";
|
||||
movieTpl.Append(optionName, accsArgs(ashdiCallUrl), "call");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
string callUrl = $"{host}/mikai/play?url={HttpUtility.UrlEncode(episode.Url)}&title={HttpUtility.UrlEncode(displayTitle)}";
|
||||
string callUrl = $"{host}/lite/mikai/play?url={HttpUtility.UrlEncode(episode.Url)}&title={HttpUtility.UrlEncode(displayTitle)}";
|
||||
movieTpl.Append(voice.DisplayName, accsArgs(callUrl), "call");
|
||||
}
|
||||
else
|
||||
@ -195,31 +196,32 @@ namespace Mikai.Controllers
|
||||
}
|
||||
|
||||
if (movieTpl.data == null || movieTpl.data.Count == 0)
|
||||
return OnError("mikai", _proxyManager);
|
||||
return OnError("mikai", refresh_proxy: true);
|
||||
|
||||
return rjson
|
||||
? Content(movieTpl.ToJson(), "application/json; charset=utf-8")
|
||||
: Content(movieTpl.ToHtml(), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
[HttpGet("mikai/play")]
|
||||
[HttpGet("lite/mikai/play")]
|
||||
public async Task<ActionResult> Play(string url, string title = null, int serial = 0)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.Mikai);
|
||||
var init = loadKit(ModInit.Mikai);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
TryEnableMagicApn(init);
|
||||
if (string.IsNullOrEmpty(url))
|
||||
return OnError("mikai", _proxyManager);
|
||||
return OnError("mikai", refresh_proxy: true);
|
||||
|
||||
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager);
|
||||
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager, httpHydra);
|
||||
OnLog($"Mikai Play: url={url}, serial={serial}");
|
||||
|
||||
string streamLink = await invoke.ResolveVideoUrl(url, serial == 1);
|
||||
if (string.IsNullOrEmpty(streamLink))
|
||||
return OnError("mikai", _proxyManager);
|
||||
return OnError("mikai", refresh_proxy: true);
|
||||
|
||||
List<HeadersModel> streamHeaders = null;
|
||||
bool forceProxy = false;
|
||||
@ -462,5 +464,56 @@ namespace Mikai.Controllers
|
||||
|
||||
return HostStreamProxy(init, link, headers: headers, force_streamproxy: forceProxy);
|
||||
}
|
||||
|
||||
private void TryEnableMagicApn(OnlinesSettings init)
|
||||
{
|
||||
if (init == null
|
||||
|| init.apn != null
|
||||
|| init.streamproxy
|
||||
|| string.IsNullOrWhiteSpace(ModInit.MagicApnAshdiHost))
|
||||
return;
|
||||
|
||||
string player = new RchClient(HttpContext, host, init, requestInfo).InfoConnected()?.player;
|
||||
bool useInnerPlayer = string.IsNullOrWhiteSpace(player)
|
||||
|| player.Equals("inner", StringComparison.OrdinalIgnoreCase);
|
||||
if (!useInnerPlayer)
|
||||
return;
|
||||
|
||||
ApnHelper.ApplyInitConf(true, ModInit.MagicApnAshdiHost, init);
|
||||
OnLog($"Mikai: увімкнено magic_apn для Ashdi (player={player ?? "unknown"}).");
|
||||
}
|
||||
|
||||
private static bool IsCheckOnlineSearchEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (checkProp?.GetValue(conf) is bool enabled)
|
||||
return enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void OnLog(string message)
|
||||
{
|
||||
System.Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
Mikai/GlobalUsings.cs
Normal file
4
Mikai/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
global using Shared.Services;
|
||||
global using Shared.Services.Hybrid;
|
||||
global using Shared.Models.Base;
|
||||
global using AppInit = Shared.CoreInit;
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -23,13 +23,15 @@ namespace Mikai
|
||||
private readonly IHybridCache _hybridCache;
|
||||
private readonly Action<string> _onLog;
|
||||
private readonly ProxyManager _proxyManager;
|
||||
private readonly HttpHydra _httpHydra;
|
||||
|
||||
public MikaiInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||
public MikaiInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
|
||||
{
|
||||
_init = init;
|
||||
_hybridCache = hybridCache;
|
||||
_onLog = onLog;
|
||||
_proxyManager = proxyManager;
|
||||
_httpHydra = httpHydra;
|
||||
}
|
||||
|
||||
public async Task<List<MikaiAnime>> Search(string title, string original_title, int year)
|
||||
@ -49,7 +51,7 @@ namespace Mikai
|
||||
var headers = DefaultHeaders();
|
||||
|
||||
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {searchUrl}");
|
||||
string json = await Http.Get(_init.cors(searchUrl), headers: headers, proxy: _proxyManager.Get());
|
||||
string json = await HttpGet(searchUrl, headers);
|
||||
if (string.IsNullOrEmpty(json))
|
||||
return null;
|
||||
|
||||
@ -93,7 +95,7 @@ namespace Mikai
|
||||
var headers = DefaultHeaders();
|
||||
|
||||
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {url}");
|
||||
string json = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||
string json = await HttpGet(url, headers);
|
||||
if (string.IsNullOrEmpty(json))
|
||||
return null;
|
||||
|
||||
@ -144,7 +146,7 @@ namespace Mikai
|
||||
};
|
||||
|
||||
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
|
||||
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
|
||||
string html = await HttpGet(requestUrl, headers);
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return null;
|
||||
|
||||
@ -190,7 +192,7 @@ namespace Mikai
|
||||
|
||||
string requestUrl = AshdiRequestUrl(WithAshdiMultivoice(url, enable: !disableAshdiMultivoiceForVod));
|
||||
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
|
||||
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
|
||||
string html = await HttpGet(requestUrl, headers);
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return streams;
|
||||
|
||||
@ -415,12 +417,20 @@ namespace Mikai
|
||||
return null;
|
||||
}
|
||||
|
||||
private Task<string> HttpGet(string url, List<HeadersModel> headers)
|
||||
{
|
||||
if (_httpHydra != null)
|
||||
return _httpHydra.Get(url, newheaders: headers);
|
||||
|
||||
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||
}
|
||||
|
||||
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
|
||||
{
|
||||
if (init != null && init.rhub && rhub != -1)
|
||||
return TimeSpan.FromMinutes(rhub);
|
||||
|
||||
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
|
||||
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
|
||||
if (ctime > multiaccess)
|
||||
ctime = multiaccess;
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models.Online.Settings;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
@ -22,12 +23,13 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Mikai
|
||||
{
|
||||
public class ModInit
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 3.8;
|
||||
public static double Version => 4.0;
|
||||
|
||||
public static OnlinesSettings Mikai;
|
||||
public static bool ApnHostProvided;
|
||||
public static string MagicApnAshdiHost;
|
||||
|
||||
public static OnlinesSettings Settings
|
||||
{
|
||||
@ -38,7 +40,7 @@ namespace Mikai
|
||||
/// <summary>
|
||||
/// модуль загружен
|
||||
/// </summary>
|
||||
public static void loaded(InitspaceModel initspace)
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
|
||||
|
||||
@ -56,15 +58,23 @@ namespace Mikai
|
||||
}
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Conf("Mikai", Mikai);
|
||||
var defaults = JObject.FromObject(Mikai);
|
||||
defaults["magic_apn"] = new JObject()
|
||||
{
|
||||
["ashdi"] = ApnHelper.DefaultHost
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Init("Mikai", defaults) ?? defaults;
|
||||
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||
MagicApnAshdiHost = ApnHelper.TryGetMagicAshdiHost(conf);
|
||||
conf.Remove("magic_apn");
|
||||
conf.Remove("apn");
|
||||
conf.Remove("apn_host");
|
||||
Mikai = conf.ToObject<OnlinesSettings>();
|
||||
if (hasApn)
|
||||
ApnHelper.ApplyInitConf(apnEnabled, apnHost, Mikai);
|
||||
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
|
||||
if (hasApn && apnEnabled)
|
||||
ApnHostProvided = ApnHelper.IsEnabled(Mikai);
|
||||
if (ApnHostProvided)
|
||||
{
|
||||
Mikai.streamproxy = false;
|
||||
}
|
||||
@ -75,7 +85,45 @@ namespace Mikai
|
||||
}
|
||||
|
||||
// Виводити "уточнити пошук"
|
||||
AppInit.conf.online.with_search.Add("mikai");
|
||||
RegisterWithSearch("mikai");
|
||||
}
|
||||
|
||||
private static void RegisterWithSearch(string plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(plugin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,47 +1,36 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Base;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mikai
|
||||
{
|
||||
public class OnlineApi
|
||||
public class OnlineApi : IModuleOnline
|
||||
{
|
||||
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||
HttpContext httpContext,
|
||||
IMemoryCache memoryCache,
|
||||
RequestModel requestInfo,
|
||||
string host,
|
||||
OnlineEventsModel args)
|
||||
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
|
||||
{
|
||||
long.TryParse(args.id, out long tmdbid);
|
||||
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||
}
|
||||
|
||||
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
private static List<ModuleOnlineItem> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
{
|
||||
var online = new List<(string name, string url, string plugin, int index)>();
|
||||
var online = new List<ModuleOnlineItem>();
|
||||
|
||||
var init = ModInit.Mikai;
|
||||
|
||||
// Визначаємо isAnime згідно стандарту Lampac (Deepwiki):
|
||||
// isanime = true якщо original_language == "ja" або "zh"
|
||||
bool hasLang = !string.IsNullOrEmpty(original_language);
|
||||
bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
|
||||
|
||||
// Mikai — аніме-провайдер. Додаємо його:
|
||||
// - при загальному пошуку (serial == -1), або
|
||||
// - якщо контент визначений як аніме (isanime), або
|
||||
// - якщо мова невідома (відсутній original_language)
|
||||
if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang))
|
||||
{
|
||||
string url = init.overridehost;
|
||||
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||
url = $"{host}/mikai";
|
||||
if (UpdateService.IsDisconnected())
|
||||
init.overridehost = null;
|
||||
|
||||
online.Add((init.displayname, url, "mikai", init.displayindex));
|
||||
online.Add(new ModuleOnlineItem(init, "mikai"));
|
||||
}
|
||||
|
||||
return online;
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "Mikai.ModInit",
|
||||
"online": "Mikai.OnlineApi"
|
||||
"enable": true
|
||||
}
|
||||
@ -25,35 +25,35 @@ namespace NMoonAnime.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("nmoonanime")]
|
||||
[Route("lite/nmoonanime")]
|
||||
public async Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string mal_id, string t, int s = -1, bool rjson = false, bool checksearch = false)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.NMoonAnime);
|
||||
var init = loadKit(ModInit.NMoonAnime);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
string effectiveMalId = ResolveMalId(mal_id, kinopoisk_id, source);
|
||||
|
||||
if (checksearch)
|
||||
{
|
||||
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
if (!IsCheckOnlineSearchEnabled())
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
var checkResults = await invoke.Search(imdb_id, effectiveMalId, title, year);
|
||||
if (checkResults != null && checkResults.Count > 0)
|
||||
return Content("data-json=", "text/plain; charset=utf-8");
|
||||
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
}
|
||||
|
||||
OnLog($"NMoonAnime: назва={title}, source={source}, imdb={imdb_id}, kinopoisk_id(як mal_id)={kinopoisk_id}, mal_id_ефективний={effectiveMalId}, рік={year}, серіал={serial}, сезон={s}, озвучка={t}");
|
||||
|
||||
var seasons = await invoke.Search(imdb_id, effectiveMalId, title, year);
|
||||
if (seasons == null || seasons.Count == 0)
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
bool isSeries = serial == 1;
|
||||
NMoonAnimeSeasonContent firstSeasonData = null;
|
||||
@ -62,7 +62,7 @@ namespace NMoonAnime.Controllers
|
||||
{
|
||||
firstSeasonData = await invoke.GetSeasonContent(seasons[0]);
|
||||
if (firstSeasonData == null || firstSeasonData.Voices.Count == 0)
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
isSeries = firstSeasonData.IsSeries;
|
||||
}
|
||||
@ -75,22 +75,22 @@ namespace NMoonAnime.Controllers
|
||||
return await RenderMovie(invoke, seasons, title, original_title, firstSeasonData, rjson);
|
||||
}
|
||||
|
||||
[HttpGet("nmoonanime/play")]
|
||||
[HttpGet("lite/nmoonanime/play")]
|
||||
public async Task<ActionResult> Play(string file, string title = null)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.NMoonAnime);
|
||||
var init = loadKit(ModInit.NMoonAnime);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(file))
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
var invoke = new NMoonAnimeInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
var streams = invoke.ParseStreams(file);
|
||||
if (streams == null || streams.Count == 0)
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
if (streams.Count == 1)
|
||||
{
|
||||
@ -107,7 +107,7 @@ namespace NMoonAnime.Controllers
|
||||
}
|
||||
|
||||
if (!streamQuality.Any())
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
var first = streamQuality.Firts();
|
||||
string json = VideoTpl.ToJson("play", first.link, title ?? string.Empty, streamquality: streamQuality);
|
||||
@ -133,7 +133,7 @@ namespace NMoonAnime.Controllers
|
||||
.ToList();
|
||||
|
||||
if (orderedSeasons.Count == 0)
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
if (selectedSeason == -1)
|
||||
{
|
||||
@ -154,14 +154,14 @@ namespace NMoonAnime.Controllers
|
||||
var currentSeason = orderedSeasons.FirstOrDefault(s => s.SeasonNumber == selectedSeason) ?? orderedSeasons[0];
|
||||
var seasonData = await invoke.GetSeasonContent(currentSeason);
|
||||
if (seasonData == null)
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
var voices = seasonData.Voices
|
||||
.Where(v => v != null && v.Episodes != null && v.Episodes.Count > 0)
|
||||
.ToList();
|
||||
|
||||
if (voices.Count == 0)
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
int activeVoiceIndex = ParseVoiceIndex(selectedVoice, voices.Count);
|
||||
var voiceTpl = new VoiceTpl(voices.Count);
|
||||
@ -180,7 +180,7 @@ namespace NMoonAnime.Controllers
|
||||
.ToList();
|
||||
|
||||
if (episodes.Count == 0)
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
string displayTitle = !string.IsNullOrWhiteSpace(title)
|
||||
? title
|
||||
@ -193,7 +193,7 @@ namespace NMoonAnime.Controllers
|
||||
{
|
||||
int episodeNumber = episode.Number <= 0 ? 1 : episode.Number;
|
||||
string episodeName = string.IsNullOrWhiteSpace(episode.Name) ? $"Епізод {episodeNumber}" : episode.Name;
|
||||
string callUrl = $"{host}/nmoonanime/play?file={HttpUtility.UrlEncode(episode.File)}&title={HttpUtility.UrlEncode(displayTitle)}";
|
||||
string callUrl = $"{host}/lite/nmoonanime/play?file={HttpUtility.UrlEncode(episode.File)}&title={HttpUtility.UrlEncode(displayTitle)}";
|
||||
episodeTpl.Append(episodeName, displayTitle, currentSeason.SeasonNumber.ToString(), episodeNumber.ToString(), accsArgs(callUrl), "call");
|
||||
}
|
||||
|
||||
@ -218,14 +218,14 @@ namespace NMoonAnime.Controllers
|
||||
.FirstOrDefault();
|
||||
|
||||
if (currentSeason == null)
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
NMoonAnimeSeasonContent seasonData = firstSeasonData;
|
||||
if (seasonData == null || !string.Equals(seasonData.Url, currentSeason.Url, StringComparison.OrdinalIgnoreCase))
|
||||
seasonData = await invoke.GetSeasonContent(currentSeason);
|
||||
|
||||
if (seasonData == null || seasonData.Voices.Count == 0)
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
string displayTitle = !string.IsNullOrWhiteSpace(title)
|
||||
? title
|
||||
@ -249,13 +249,13 @@ namespace NMoonAnime.Controllers
|
||||
continue;
|
||||
|
||||
string voiceName = string.IsNullOrWhiteSpace(voice.Name) ? $"Озвучка {fallbackIndex}" : voice.Name;
|
||||
string callUrl = $"{host}/nmoonanime/play?file={HttpUtility.UrlEncode(file)}&title={HttpUtility.UrlEncode(displayTitle)}";
|
||||
string callUrl = $"{host}/lite/nmoonanime/play?file={HttpUtility.UrlEncode(file)}&title={HttpUtility.UrlEncode(displayTitle)}";
|
||||
movieTpl.Append(voiceName, accsArgs(callUrl), "call");
|
||||
fallbackIndex++;
|
||||
}
|
||||
|
||||
if (movieTpl.IsEmpty)
|
||||
return OnError("nmoonanime", proxyManager);
|
||||
return OnError("nmoonanime", refresh_proxy: true);
|
||||
|
||||
return rjson
|
||||
? Content(movieTpl.ToJson(), "application/json; charset=utf-8")
|
||||
@ -265,7 +265,7 @@ namespace NMoonAnime.Controllers
|
||||
private string BuildIndexUrl(string imdbId, long kinopoiskId, string title, string originalTitle, int year, int serial, string malId, int season, string voice)
|
||||
{
|
||||
var url = new StringBuilder();
|
||||
url.Append($"{host}/nmoonanime?imdb_id={HttpUtility.UrlEncode(imdbId)}");
|
||||
url.Append($"{host}/lite/nmoonanime?imdb_id={HttpUtility.UrlEncode(imdbId)}");
|
||||
url.Append($"&kinopoisk_id={kinopoiskId}");
|
||||
url.Append($"&title={HttpUtility.UrlEncode(title)}");
|
||||
url.Append($"&original_title={HttpUtility.UrlEncode(originalTitle)}");
|
||||
@ -353,5 +353,38 @@ namespace NMoonAnime.Controllers
|
||||
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private static bool IsCheckOnlineSearchEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (checkProp?.GetValue(conf) is bool enabled)
|
||||
return enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void OnLog(string message)
|
||||
{
|
||||
System.Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
NMoonAnime/GlobalUsings.cs
Normal file
4
NMoonAnime/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
global using Shared.Services;
|
||||
global using Shared.Services.Hybrid;
|
||||
global using Shared.Models.Base;
|
||||
global using AppInit = Shared.CoreInit;
|
||||
@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using Shared.Models.Online.Settings;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
@ -16,9 +17,9 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace NMoonAnime
|
||||
{
|
||||
public class ModInit
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 1.0;
|
||||
public static double Version => 2.0;
|
||||
|
||||
public static OnlinesSettings NMoonAnime;
|
||||
|
||||
@ -33,7 +34,7 @@ namespace NMoonAnime
|
||||
/// <summary>
|
||||
/// Модуль завантажено.
|
||||
/// </summary>
|
||||
public static void loaded(InitspaceModel initspace)
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
NMoonAnime = new OnlinesSettings("NMoonAnime", "https://moonanime.art", "https://apx.lme.isroot.in", streamproxy: false, useproxy: false)
|
||||
{
|
||||
@ -48,7 +49,7 @@ namespace NMoonAnime
|
||||
}
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Conf("NMoonAnime", NMoonAnime) ?? JObject.FromObject(NMoonAnime);
|
||||
var conf = ModuleInvoke.Init("NMoonAnime", JObject.FromObject(NMoonAnime)) ?? JObject.FromObject(NMoonAnime);
|
||||
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||
conf.Remove("apn");
|
||||
conf.Remove("apn_host");
|
||||
@ -68,7 +69,45 @@ namespace NMoonAnime
|
||||
NMoonAnime.apn = null;
|
||||
}
|
||||
|
||||
AppInit.conf.online.with_search.Add("nmoonanime");
|
||||
RegisterWithSearch("nmoonanime");
|
||||
}
|
||||
|
||||
private static void RegisterWithSearch(string plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(plugin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -22,6 +22,7 @@ namespace NMoonAnime
|
||||
private readonly IHybridCache _hybridCache;
|
||||
private readonly Action<string> _onLog;
|
||||
private readonly ProxyManager _proxyManager;
|
||||
private readonly HttpHydra _httpHydra;
|
||||
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
@ -35,12 +36,13 @@ namespace NMoonAnime
|
||||
private static readonly UTF8Encoding _utf8Strict = new UTF8Encoding(false, true);
|
||||
private static readonly Encoding _latin1 = Encoding.GetEncoding("ISO-8859-1");
|
||||
|
||||
public NMoonAnimeInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||
public NMoonAnimeInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
|
||||
{
|
||||
_init = init;
|
||||
_hybridCache = hybridCache;
|
||||
_onLog = onLog;
|
||||
_proxyManager = proxyManager;
|
||||
_httpHydra = httpHydra;
|
||||
}
|
||||
|
||||
public async Task<List<NMoonAnimeSeasonRef>> Search(string imdbId, string malId, string title, int year)
|
||||
@ -64,7 +66,7 @@ namespace NMoonAnime
|
||||
continue;
|
||||
|
||||
_onLog($"NMoonAnime: пошук через {searchUrl}");
|
||||
string json = await Http.Get(_init.cors(searchUrl), headers: DefaultHeaders(), proxy: _proxyManager.Get());
|
||||
string json = await HttpGet(searchUrl, DefaultHeaders());
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
continue;
|
||||
|
||||
@ -108,7 +110,7 @@ namespace NMoonAnime
|
||||
try
|
||||
{
|
||||
_onLog($"NMoonAnime: завантаження сезону {season.Url}");
|
||||
string html = await Http.Get(_init.cors(season.Url), headers: DefaultHeaders(), proxy: _proxyManager.Get());
|
||||
string html = await HttpGet(season.Url, DefaultHeaders());
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
return null;
|
||||
|
||||
@ -1098,12 +1100,20 @@ namespace NMoonAnime
|
||||
};
|
||||
}
|
||||
|
||||
private Task<string> HttpGet(string url, List<HeadersModel> headers)
|
||||
{
|
||||
if (_httpHydra != null)
|
||||
return _httpHydra.Get(url, newheaders: headers);
|
||||
|
||||
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||
}
|
||||
|
||||
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
|
||||
{
|
||||
if (init != null && init.rhub && rhub != -1)
|
||||
return TimeSpan.FromMinutes(rhub);
|
||||
|
||||
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
|
||||
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
|
||||
if (ctime > multiaccess)
|
||||
ctime = multiaccess;
|
||||
|
||||
|
||||
@ -1,28 +1,24 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Base;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NMoonAnime
|
||||
{
|
||||
public class OnlineApi
|
||||
public class OnlineApi : IModuleOnline
|
||||
{
|
||||
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||
HttpContext httpContext,
|
||||
IMemoryCache memoryCache,
|
||||
RequestModel requestInfo,
|
||||
string host,
|
||||
OnlineEventsModel args)
|
||||
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
|
||||
{
|
||||
long.TryParse(args.id, out long tmdbid);
|
||||
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||
}
|
||||
|
||||
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
private static List<ModuleOnlineItem> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
{
|
||||
var online = new List<(string name, string url, string plugin, int index)>();
|
||||
var online = new List<ModuleOnlineItem>();
|
||||
|
||||
var init = ModInit.NMoonAnime;
|
||||
|
||||
@ -31,11 +27,10 @@ namespace NMoonAnime
|
||||
|
||||
if (init.enable && !init.rip && (serial == -1 || isAnime || !hasLang))
|
||||
{
|
||||
string url = init.overridehost;
|
||||
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||
url = $"{host}/nmoonanime";
|
||||
if (UpdateService.IsDisconnected())
|
||||
init.overridehost = null;
|
||||
|
||||
online.Add((init.displayname, url, "nmoonanime", init.displayindex));
|
||||
online.Add(new ModuleOnlineItem(init, "nmoonanime"));
|
||||
}
|
||||
|
||||
return online;
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "NMoonAnime.ModInit",
|
||||
"online": "NMoonAnime.OnlineApi"
|
||||
"enable": true
|
||||
}
|
||||
|
||||
10
README.md
10
README.md
@ -1,4 +1,4 @@
|
||||
# Ukraine online source for Lampac
|
||||
# Ukraine online source for Lampac NextGen
|
||||
|
||||
## Sources
|
||||
### TVShows and Movies
|
||||
@ -76,15 +76,17 @@ modules - optional, if not specified, all modules from the repository will be in
|
||||
]
|
||||
},
|
||||
"displayindex": 1,
|
||||
"apn": true,
|
||||
"apn_host": "domaine.com/{encodeurl}"
|
||||
"magic_apn": {
|
||||
"ashdi": "https://tut.im/proxy.php?url={encodeurl}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Parameter compatibility:
|
||||
- `webcorshost` + `useproxy`: work together (parsing via CORS host, and network output can go through a proxy with `useproxy`).
|
||||
- `webcorshost` does not conflict with `streamproxy`: CORS is used for parsing, `streamproxy` is used for streaming.
|
||||
- `webcorshost` does not conflict with `apn`: APN is used at the streaming stage, not for regular parsing.
|
||||
- `magic_apn.ashdi` використовується тільки для Ashdi-посилань і лише коли значення непорожнє.
|
||||
- `webcorshost` не конфліктує з `magic_apn`: CORS використовується для парсингу, `magic_apn` — для Ashdi-стрімінгу.
|
||||
|
||||
## JackTor config example (`init.conf`)
|
||||
|
||||
|
||||
@ -24,27 +24,27 @@ namespace StarLight.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("starlight")]
|
||||
[Route("lite/starlight")]
|
||||
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, int s = -1, bool rjson = false, string href = null, bool checksearch = false)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.StarLight);
|
||||
var init = loadKit(ModInit.StarLight);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
|
||||
if (checksearch)
|
||||
{
|
||||
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||
return OnError("starlight", proxyManager);
|
||||
if (!IsCheckOnlineSearchEnabled())
|
||||
return OnError("starlight", refresh_proxy: true);
|
||||
|
||||
var searchResults = await invoke.Search(title, original_title);
|
||||
if (searchResults != null && searchResults.Count > 0)
|
||||
return Content("data-json=", "text/plain; charset=utf-8");
|
||||
|
||||
return OnError("starlight", proxyManager);
|
||||
return OnError("starlight", refresh_proxy: true);
|
||||
}
|
||||
|
||||
string itemUrl = href;
|
||||
@ -52,14 +52,14 @@ namespace StarLight.Controllers
|
||||
{
|
||||
var searchResults = await invoke.Search(title, original_title);
|
||||
if (searchResults == null || searchResults.Count == 0)
|
||||
return OnError("starlight", proxyManager);
|
||||
return OnError("starlight", refresh_proxy: true);
|
||||
|
||||
if (searchResults.Count > 1)
|
||||
{
|
||||
var similar_tpl = new SimilarTpl(searchResults.Count);
|
||||
foreach (var res in searchResults)
|
||||
{
|
||||
string link = $"{host}/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Href)}";
|
||||
string link = $"{host}/lite/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Href)}";
|
||||
similar_tpl.Append(res.Title, string.Empty, string.Empty, link, string.Empty);
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ namespace StarLight.Controllers
|
||||
|
||||
var project = await invoke.GetProject(itemUrl);
|
||||
if (project == null)
|
||||
return OnError("starlight", proxyManager);
|
||||
return OnError("starlight", refresh_proxy: true);
|
||||
|
||||
if (serial == 1 && project.Seasons.Count > 0)
|
||||
{
|
||||
@ -82,7 +82,7 @@ namespace StarLight.Controllers
|
||||
{
|
||||
var seasonInfo = project.Seasons[i];
|
||||
string seasonName = string.IsNullOrEmpty(seasonInfo.Title) ? $"Сезон {i + 1}" : seasonInfo.Title;
|
||||
string link = $"{host}/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={i}&href={HttpUtility.UrlEncode(itemUrl)}";
|
||||
string link = $"{host}/lite/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={i}&href={HttpUtility.UrlEncode(itemUrl)}";
|
||||
season_tpl.Append(seasonName, link, i.ToString());
|
||||
}
|
||||
|
||||
@ -90,13 +90,13 @@ namespace StarLight.Controllers
|
||||
}
|
||||
|
||||
if (s < 0 || s >= project.Seasons.Count)
|
||||
return OnError("starlight", proxyManager);
|
||||
return OnError("starlight", refresh_proxy: true);
|
||||
|
||||
var season = project.Seasons[s];
|
||||
string seasonSlug = season.Slug;
|
||||
var episodes = invoke.GetEpisodes(project, seasonSlug);
|
||||
if (episodes == null || episodes.Count == 0)
|
||||
return OnError("starlight", proxyManager);
|
||||
return OnError("starlight", refresh_proxy: true);
|
||||
|
||||
var episode_tpl = new EpisodeTpl();
|
||||
int index = 1;
|
||||
@ -114,7 +114,7 @@ namespace StarLight.Controllers
|
||||
continue;
|
||||
|
||||
string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {index}" : ep.Title;
|
||||
string callUrl = $"{host}/starlight/play?hash={HttpUtility.UrlEncode(ep.Hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
|
||||
string callUrl = $"{host}/lite/starlight/play?hash={HttpUtility.UrlEncode(ep.Hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
|
||||
episode_tpl.Append(episodeName, title ?? original_title, seasonNumber, index.ToString("D2"), accsArgs(callUrl), "call");
|
||||
index++;
|
||||
}
|
||||
@ -128,9 +128,9 @@ namespace StarLight.Controllers
|
||||
hash = project.Episodes.FirstOrDefault(e => !string.IsNullOrEmpty(e.Hash))?.Hash;
|
||||
|
||||
if (string.IsNullOrEmpty(hash))
|
||||
return OnError("starlight", proxyManager);
|
||||
return OnError("starlight", refresh_proxy: true);
|
||||
|
||||
string callUrl = $"{host}/starlight/play?hash={HttpUtility.UrlEncode(hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
|
||||
string callUrl = $"{host}/lite/starlight/play?hash={HttpUtility.UrlEncode(hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
|
||||
var movie_tpl = new MovieTpl(title, original_title, 1);
|
||||
movie_tpl.Append(string.IsNullOrEmpty(title) ? "StarLight" : title, accsArgs(callUrl), "call");
|
||||
|
||||
@ -139,22 +139,22 @@ namespace StarLight.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("starlight/play")]
|
||||
[Route("lite/starlight/play")]
|
||||
async public Task<ActionResult> Play(string hash, string title)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
if (string.IsNullOrEmpty(hash))
|
||||
return OnError("starlight", proxyManager);
|
||||
return OnError("starlight", refresh_proxy: true);
|
||||
|
||||
var init = await loadKit(ModInit.StarLight);
|
||||
var init = loadKit(ModInit.StarLight);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
var result = await invoke.ResolveStream(hash);
|
||||
if (result == null || string.IsNullOrEmpty(result.Stream))
|
||||
return OnError("starlight", proxyManager);
|
||||
return OnError("starlight", refresh_proxy: true);
|
||||
|
||||
string videoTitle = title ?? result.Name ?? "";
|
||||
|
||||
@ -266,5 +266,38 @@ namespace StarLight.Controllers
|
||||
|
||||
return DateTime.TryParse(episode.Date, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt) ? dt : null;
|
||||
}
|
||||
|
||||
private static bool IsCheckOnlineSearchEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (checkProp?.GetValue(conf) is bool enabled)
|
||||
return enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void OnLog(string message)
|
||||
{
|
||||
System.Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
StarLight/GlobalUsings.cs
Normal file
4
StarLight/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
global using Shared.Services;
|
||||
global using Shared.Services.Hybrid;
|
||||
global using Shared.Models.Base;
|
||||
global using AppInit = Shared.CoreInit;
|
||||
@ -2,6 +2,7 @@ using Newtonsoft.Json.Linq;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using Shared.Models.Online.Settings;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
@ -22,9 +23,9 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace StarLight
|
||||
{
|
||||
public class ModInit
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 3.3;
|
||||
public static double Version => 4.0;
|
||||
|
||||
public static OnlinesSettings StarLight;
|
||||
public static bool ApnHostProvided;
|
||||
@ -38,7 +39,7 @@ namespace StarLight
|
||||
/// <summary>
|
||||
/// модуль загружен
|
||||
/// </summary>
|
||||
public static void loaded(InitspaceModel initspace)
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
|
||||
|
||||
@ -54,7 +55,7 @@ namespace StarLight
|
||||
list = new string[] { "socks5://ip:port" }
|
||||
}
|
||||
};
|
||||
var conf = ModuleInvoke.Conf("StarLight", StarLight);
|
||||
var conf = ModuleInvoke.Init("StarLight", JObject.FromObject(StarLight));
|
||||
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||
conf.Remove("apn");
|
||||
conf.Remove("apn_host");
|
||||
@ -73,7 +74,45 @@ namespace StarLight
|
||||
}
|
||||
|
||||
// Виводити "уточнити пошук"
|
||||
AppInit.conf.online.with_search.Add("starlight");
|
||||
RegisterWithSearch("starlight");
|
||||
}
|
||||
|
||||
private static void RegisterWithSearch(string plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(plugin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,29 +1,25 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Base;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StarLight
|
||||
{
|
||||
public class OnlineApi
|
||||
public class OnlineApi : IModuleOnline
|
||||
{
|
||||
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||
HttpContext httpContext,
|
||||
IMemoryCache memoryCache,
|
||||
RequestModel requestInfo,
|
||||
string host,
|
||||
OnlineEventsModel args)
|
||||
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
|
||||
{
|
||||
long.TryParse(args.id, out long tmdbid);
|
||||
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||
}
|
||||
|
||||
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
private static List<ModuleOnlineItem> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
{
|
||||
var online = new List<(string name, string url, string plugin, int index)>();
|
||||
var online = new List<ModuleOnlineItem>();
|
||||
|
||||
if (!string.Equals(original_language, "uk", StringComparison.OrdinalIgnoreCase))
|
||||
return online;
|
||||
@ -31,11 +27,10 @@ namespace StarLight
|
||||
var init = ModInit.StarLight;
|
||||
if (init.enable && !init.rip)
|
||||
{
|
||||
string url = init.overridehost;
|
||||
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||
url = $"{host}/starlight";
|
||||
if (UpdateService.IsDisconnected())
|
||||
init.overridehost = null;
|
||||
|
||||
online.Add((init.displayname, url, "starlight", init.displayindex));
|
||||
online.Add(new ModuleOnlineItem(init, "starlight"));
|
||||
}
|
||||
|
||||
return online;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -24,13 +24,15 @@ namespace StarLight
|
||||
private readonly IHybridCache _hybridCache;
|
||||
private readonly Action<string> _onLog;
|
||||
private readonly ProxyManager _proxyManager;
|
||||
private readonly HttpHydra _httpHydra;
|
||||
|
||||
public StarLightInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||
public StarLightInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
|
||||
{
|
||||
_init = init;
|
||||
_hybridCache = hybridCache;
|
||||
_onLog = onLog;
|
||||
_proxyManager = proxyManager;
|
||||
_httpHydra = httpHydra;
|
||||
}
|
||||
|
||||
public async Task<List<SearchResult>> Search(string title, string original_title)
|
||||
@ -54,7 +56,7 @@ namespace StarLight
|
||||
try
|
||||
{
|
||||
_onLog?.Invoke($"StarLight search: {url}");
|
||||
string payload = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||
string payload = await HttpGet(url, headers);
|
||||
if (string.IsNullOrEmpty(payload))
|
||||
return null;
|
||||
|
||||
@ -112,7 +114,7 @@ namespace StarLight
|
||||
try
|
||||
{
|
||||
_onLog?.Invoke($"StarLight project: {href}");
|
||||
string payload = await Http.Get(_init.cors(href), headers: headers, proxy: _proxyManager.Get());
|
||||
string payload = await HttpGet(href, headers);
|
||||
if (string.IsNullOrEmpty(payload))
|
||||
return null;
|
||||
|
||||
@ -193,7 +195,7 @@ namespace StarLight
|
||||
try
|
||||
{
|
||||
_onLog?.Invoke($"StarLight season: {seasonUrl}");
|
||||
string payload = await Http.Get(_init.cors(seasonUrl), headers: headers, proxy: _proxyManager.Get());
|
||||
string payload = await HttpGet(seasonUrl, headers);
|
||||
if (string.IsNullOrEmpty(payload))
|
||||
continue;
|
||||
|
||||
@ -279,7 +281,7 @@ namespace StarLight
|
||||
try
|
||||
{
|
||||
_onLog?.Invoke($"StarLight stream: {url}");
|
||||
string payload = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||
string payload = await HttpGet(url, headers);
|
||||
if (string.IsNullOrEmpty(payload))
|
||||
return null;
|
||||
|
||||
@ -338,12 +340,20 @@ namespace StarLight
|
||||
return $"{_init.host}{path}";
|
||||
}
|
||||
|
||||
private Task<string> HttpGet(string url, List<HeadersModel> headers)
|
||||
{
|
||||
if (_httpHydra != null)
|
||||
return _httpHydra.Get(url, newheaders: headers);
|
||||
|
||||
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||
}
|
||||
|
||||
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
|
||||
{
|
||||
if (init != null && init.rhub && rhub != -1)
|
||||
return TimeSpan.FromMinutes(rhub);
|
||||
|
||||
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
|
||||
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
|
||||
if (ctime > multiaccess)
|
||||
ctime = multiaccess;
|
||||
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "StarLight.ModInit",
|
||||
"online": "StarLight.OnlineApi"
|
||||
"enable": true
|
||||
}
|
||||
99
UafilmME/ApnHelper.cs
Normal file
99
UafilmME/ApnHelper.cs
Normal 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
356
UafilmME/Controller.cs
Normal 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
4
UafilmME/GlobalUsings.cs
Normal 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
228
UafilmME/ModInit.cs
Normal 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);
|
||||
}
|
||||
67
UafilmME/Models/UafilmModels.cs
Normal file
67
UafilmME/Models/UafilmModels.cs
Normal 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
33
UafilmME/OnlineApi.cs
Normal 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
15
UafilmME/UafilmME.csproj
Normal 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
751
UafilmME/UafilmMEInvoke.cs
Normal 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
6
UafilmME/manifest.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "UafilmME.ModInit",
|
||||
"online": "UafilmME.OnlineApi"
|
||||
}
|
||||
@ -25,6 +25,23 @@ namespace Shared.Engine
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string TryGetMagicAshdiHost(JObject conf)
|
||||
{
|
||||
if (conf == null || !conf.TryGetValue("magic_apn", out var magicToken) || magicToken == null)
|
||||
return null;
|
||||
|
||||
if (magicToken.Type == JTokenType.Boolean)
|
||||
return magicToken.Value<bool>() ? DefaultHost : null;
|
||||
|
||||
if (magicToken.Type == JTokenType.String)
|
||||
return NormalizeHost(magicToken.Value<string>());
|
||||
|
||||
if (magicToken.Type != JTokenType.Object)
|
||||
return null;
|
||||
|
||||
return NormalizeHost(((JObject)magicToken).Value<string>("ashdi"));
|
||||
}
|
||||
|
||||
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
|
||||
{
|
||||
if (init == null)
|
||||
@ -37,8 +54,13 @@ namespace Shared.Engine
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
host = DefaultHost;
|
||||
host = NormalizeHost(host);
|
||||
if (host == null)
|
||||
{
|
||||
init.apnstream = false;
|
||||
init.apn = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (init.apn == null)
|
||||
init.apn = new ApnConf();
|
||||
@ -82,5 +104,13 @@ namespace Shared.Engine
|
||||
|
||||
return $"{host.TrimEnd('/')}/{url}";
|
||||
}
|
||||
|
||||
private static string NormalizeHost(string host)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
return null;
|
||||
|
||||
return host.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,28 +28,29 @@ namespace Uaflix.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("uaflix")]
|
||||
[Route("lite/uaflix")]
|
||||
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, int e = -1, bool play = false, bool rjson = false, string href = null, bool checksearch = false)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.UaFlix);
|
||||
if (await IsBadInitialization(init))
|
||||
return Forbid();
|
||||
if (await IsRequestBlocked(rch: false))
|
||||
return badInitMsg;
|
||||
|
||||
var init = this.init;
|
||||
TryEnableMagicApn(init);
|
||||
OnLog($"=== UAFLIX INDEX START ===");
|
||||
OnLog($"Uaflix Index: title={title}, serial={serial}, s={s}, play={play}, href={href}, checksearch={checksearch}");
|
||||
OnLog($"Uaflix Index: kinopoisk_id={kinopoisk_id}, imdb_id={imdb_id}, id={id}");
|
||||
OnLog($"Uaflix Index: year={year}, source={source}, t={t}, e={e}, rjson={rjson}");
|
||||
|
||||
var auth = new UaflixAuth(init, memoryCache, OnLog, proxyManager);
|
||||
var invoke = new UaflixInvoke(init, hybridCache, OnLog, proxyManager, auth);
|
||||
var invoke = new UaflixInvoke(init, hybridCache, OnLog, proxyManager, auth, httpHydra);
|
||||
|
||||
// Обробка параметра checksearch - повертаємо спеціальну відповідь для валідації
|
||||
if (checksearch)
|
||||
{
|
||||
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||
return OnError("uaflix", proxyManager);
|
||||
if (!IsCheckOnlineSearchEnabled())
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
|
||||
try
|
||||
{
|
||||
@ -63,13 +64,13 @@ namespace Uaflix.Controllers
|
||||
|
||||
OnLog("checksearch: Контент не знайдено");
|
||||
OnLog("=== RETURN: checksearch OnError ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnLog($"checksearch: помилка - {ex.Message}");
|
||||
OnLog("=== RETURN: checksearch exception OnError ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +81,7 @@ namespace Uaflix.Controllers
|
||||
if (string.IsNullOrWhiteSpace(urlToParse))
|
||||
{
|
||||
OnLog("=== RETURN: play missing url OnError ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var playResult = await invoke.ParseEpisode(urlToParse);
|
||||
@ -91,7 +92,7 @@ namespace Uaflix.Controllers
|
||||
}
|
||||
|
||||
OnLog("=== RETURN: play no streams ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
// Якщо є episode_url але немає play=true, це виклик для отримання інформації про стрім (для method: 'call')
|
||||
@ -109,7 +110,7 @@ namespace Uaflix.Controllers
|
||||
}
|
||||
|
||||
OnLog("=== RETURN: call method no streams ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
string filmUrl = href;
|
||||
@ -121,7 +122,7 @@ namespace Uaflix.Controllers
|
||||
{
|
||||
OnLog("No search results found");
|
||||
OnLog("=== RETURN: no search results OnError ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var selectedResult = invoke.SelectBestSearchResult(searchResults, title, original_title, year);
|
||||
@ -142,7 +143,7 @@ namespace Uaflix.Controllers
|
||||
var similar_tpl = new SimilarTpl(orderedResults.Count);
|
||||
foreach (var res in orderedResults)
|
||||
{
|
||||
string link = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}";
|
||||
string link = $"{host}/lite/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}";
|
||||
string y = res.Year > 0 ? res.Year.ToString() : string.Empty;
|
||||
string details = res.Category switch
|
||||
{
|
||||
@ -162,97 +163,50 @@ namespace Uaflix.Controllers
|
||||
|
||||
if (serial == 1)
|
||||
{
|
||||
// Агрегуємо всі озвучки з усіх плеєрів
|
||||
var structure = await invoke.AggregateSerialStructure(filmUrl);
|
||||
if (structure == null || !structure.Voices.Any())
|
||||
{
|
||||
OnLog("No voices found in aggregated structure");
|
||||
OnLog("=== RETURN: no voices OnError ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
}
|
||||
|
||||
OnLog($"Structure aggregated successfully: {structure.Voices.Count} voices, URL: {filmUrl}");
|
||||
foreach (var voice in structure.Voices)
|
||||
{
|
||||
OnLog($"Voice: {voice.Key}, Type: {voice.Value.PlayerType}, Seasons: {voice.Value.Seasons.Count}");
|
||||
foreach (var season in voice.Value.Seasons)
|
||||
{
|
||||
OnLog($" Season {season.Key}: {season.Value.Count} episodes");
|
||||
}
|
||||
}
|
||||
|
||||
// s == -1: Вибір сезону
|
||||
// s == -1: швидкий вибір сезону без повної агрегації серіалу
|
||||
if (s == -1)
|
||||
{
|
||||
List<int> allSeasons;
|
||||
VoiceInfo tVoice = null;
|
||||
bool restrictByVoice = !string.IsNullOrEmpty(t) && structure.Voices.TryGetValue(t, out tVoice) && IsAshdiVoice(tVoice);
|
||||
if (restrictByVoice)
|
||||
{
|
||||
allSeasons = GetSeasonSet(tVoice).OrderBy(sn => sn).ToList();
|
||||
OnLog($"Ashdi voice selected (t='{t}'), seasons count={allSeasons.Count}");
|
||||
}
|
||||
else
|
||||
{
|
||||
allSeasons = structure.Voices
|
||||
.SelectMany(v => GetSeasonSet(v.Value))
|
||||
var seasonIndex = await invoke.GetSeasonIndex(filmUrl);
|
||||
var seasons = seasonIndex?.Seasons?.Keys
|
||||
.Distinct()
|
||||
.OrderBy(sn => sn)
|
||||
.ToList();
|
||||
|
||||
if (seasons == null || seasons.Count == 0)
|
||||
{
|
||||
OnLog("No seasons found in season index");
|
||||
OnLog("=== RETURN: no seasons OnError ===");
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
OnLog($"Found {allSeasons.Count} seasons in structure: {string.Join(", ", allSeasons)}");
|
||||
|
||||
// Перевіряємо чи сезони містять валідні епізоди з файлами
|
||||
var seasonsWithValidEpisodes = allSeasons.Where(season =>
|
||||
structure.Voices.Values.Any(v =>
|
||||
v.Seasons.ContainsKey(season) &&
|
||||
v.Seasons[season].Any(ep => !string.IsNullOrEmpty(ep.File))
|
||||
)
|
||||
).ToList();
|
||||
|
||||
OnLog($"Seasons with valid episodes: {seasonsWithValidEpisodes.Count}");
|
||||
foreach (var season in allSeasons)
|
||||
var season_tpl = new SeasonTpl(seasons.Count);
|
||||
foreach (int season in seasons)
|
||||
{
|
||||
var episodesInSeason = structure.Voices.Values
|
||||
.Where(v => v.Seasons.ContainsKey(season))
|
||||
.SelectMany(v => v.Seasons[season])
|
||||
.Where(ep => !string.IsNullOrEmpty(ep.File))
|
||||
.ToList();
|
||||
OnLog($"Season {season}: {episodesInSeason.Count} valid episodes");
|
||||
}
|
||||
|
||||
if (!seasonsWithValidEpisodes.Any())
|
||||
{
|
||||
OnLog("No seasons with valid episodes found in structure");
|
||||
OnLog("=== RETURN: no valid seasons OnError ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
}
|
||||
|
||||
var season_tpl = new SeasonTpl(seasonsWithValidEpisodes.Count);
|
||||
foreach (var season in seasonsWithValidEpisodes)
|
||||
{
|
||||
string link = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season}&href={HttpUtility.UrlEncode(filmUrl)}";
|
||||
if (restrictByVoice)
|
||||
string link = $"{host}/lite/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season}&href={HttpUtility.UrlEncode(filmUrl)}";
|
||||
if (!string.IsNullOrWhiteSpace(t))
|
||||
link += $"&t={HttpUtility.UrlEncode(t)}";
|
||||
|
||||
season_tpl.Append($"{season}", link, season.ToString());
|
||||
OnLog($"Added season {season} to template");
|
||||
}
|
||||
|
||||
OnLog($"Returning season template with {seasonsWithValidEpisodes.Count} seasons");
|
||||
|
||||
var htmlContent = rjson ? season_tpl.ToJson() : season_tpl.ToHtml();
|
||||
OnLog($"Season template response length: {htmlContent.Length}");
|
||||
OnLog($"Season template HTML (first 300): {htmlContent.Substring(0, Math.Min(300, htmlContent.Length))}");
|
||||
OnLog($"=== RETURN: season template ({seasonsWithValidEpisodes.Count} seasons) ===");
|
||||
|
||||
return Content(htmlContent, rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8");
|
||||
OnLog($"=== RETURN: season template ({seasons.Count} seasons) ===");
|
||||
return Content(
|
||||
rjson ? season_tpl.ToJson() : season_tpl.ToHtml(),
|
||||
rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8"
|
||||
);
|
||||
}
|
||||
// s >= 0: Показуємо озвучки + епізоди
|
||||
else if (s >= 0)
|
||||
|
||||
// s >= 0: завантажуємо тільки потрібний сезон
|
||||
if (s >= 0)
|
||||
{
|
||||
var structure = await invoke.GetSeasonStructure(filmUrl, s);
|
||||
if (structure == null || structure.Voices == null || structure.Voices.Count == 0)
|
||||
{
|
||||
OnLog($"No voices found for season {s}");
|
||||
OnLog("=== RETURN: no voices for season OnError ===");
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
var voicesForSeason = structure.Voices
|
||||
.Where(v => v.Value.Seasons.ContainsKey(s))
|
||||
.Select(v => new { DisplayName = v.Key, Info = v.Value })
|
||||
.ToList();
|
||||
|
||||
@ -260,7 +214,7 @@ namespace Uaflix.Controllers
|
||||
{
|
||||
OnLog($"No voices found for season {s}");
|
||||
OnLog("=== RETURN: no voices for season OnError ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
// Автоматично вибираємо першу озвучку якщо не вказана
|
||||
@ -275,23 +229,27 @@ namespace Uaflix.Controllers
|
||||
OnLog($"Voice '{t}' not found, fallback to first voice: {t}");
|
||||
}
|
||||
|
||||
VoiceInfo selectedVoice = null;
|
||||
if (!structure.Voices.TryGetValue(t, out selectedVoice) || !selectedVoice.Seasons.ContainsKey(s) || selectedVoice.Seasons[s] == null || selectedVoice.Seasons[s].Count == 0)
|
||||
{
|
||||
var fallbackVoice = voicesForSeason.FirstOrDefault(v => v.Info.Seasons.ContainsKey(s) && v.Info.Seasons[s] != null && v.Info.Seasons[s].Count > 0);
|
||||
if (fallbackVoice == null)
|
||||
{
|
||||
OnLog($"Season {s} not found for selected voice and fallback voice missing");
|
||||
OnLog("=== RETURN: season not found for voice OnError ===");
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
t = fallbackVoice.DisplayName;
|
||||
selectedVoice = fallbackVoice.Info;
|
||||
OnLog($"Selected voice had no episodes, fallback to: {t}");
|
||||
}
|
||||
|
||||
// Створюємо VoiceTpl з усіма озвучками
|
||||
var voice_tpl = new VoiceTpl();
|
||||
var selectedVoiceInfo = structure.Voices[t];
|
||||
var selectedSeasonSet = GetSeasonSet(selectedVoiceInfo);
|
||||
bool selectedIsAshdi = IsAshdiVoice(selectedVoiceInfo);
|
||||
|
||||
foreach (var voice in voicesForSeason)
|
||||
{
|
||||
bool targetIsAshdi = IsAshdiVoice(voice.Info);
|
||||
var targetSeasonSet = GetSeasonSet(voice.Info);
|
||||
bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet);
|
||||
bool needSeasonReset = (selectedIsAshdi || targetIsAshdi) && !sameSeasonSet;
|
||||
|
||||
string voiceLink = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&href={HttpUtility.UrlEncode(filmUrl)}";
|
||||
if (needSeasonReset)
|
||||
voiceLink += $"&s=-1&t={HttpUtility.UrlEncode(voice.DisplayName)}";
|
||||
else
|
||||
string voiceLink = $"{host}/lite/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&href={HttpUtility.UrlEncode(filmUrl)}";
|
||||
voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.DisplayName)}";
|
||||
|
||||
bool isActive = voice.DisplayName == t;
|
||||
@ -299,44 +257,28 @@ namespace Uaflix.Controllers
|
||||
}
|
||||
OnLog($"Created VoiceTpl with {voicesForSeason.Count} voices, active: {t}");
|
||||
|
||||
// Відображення епізодів для вибраної озвучки
|
||||
if (!structure.Voices.ContainsKey(t))
|
||||
{
|
||||
OnLog($"Voice '{t}' not found in structure");
|
||||
OnLog("=== RETURN: voice not found OnError ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
}
|
||||
|
||||
if (!structure.Voices[t].Seasons.ContainsKey(s))
|
||||
{
|
||||
OnLog($"Season {s} not found for voice '{t}'");
|
||||
if (IsAshdiVoice(structure.Voices[t]))
|
||||
{
|
||||
string redirectUrl = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s=-1&t={HttpUtility.UrlEncode(t)}&href={HttpUtility.UrlEncode(filmUrl)}";
|
||||
OnLog($"Ashdi voice missing season, redirect to season selector: {redirectUrl}");
|
||||
return Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
OnLog("=== RETURN: season not found for voice OnError ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
}
|
||||
|
||||
var episodes = structure.Voices[t].Seasons[s];
|
||||
var episodes = selectedVoice.Seasons[s];
|
||||
var episode_tpl = new EpisodeTpl();
|
||||
int appendedEpisodes = 0;
|
||||
|
||||
foreach (var ep in episodes)
|
||||
{
|
||||
if (ep == null || string.IsNullOrWhiteSpace(ep.File))
|
||||
continue;
|
||||
|
||||
string episodeTitle = !string.IsNullOrWhiteSpace(ep.Title) ? ep.Title : $"Епізод {ep.Number}";
|
||||
|
||||
// Для zetvideo-vod повертаємо URL епізоду з методом call
|
||||
// Для ashdi/zetvideo-serial повертаємо готове посилання з play
|
||||
var voice = structure.Voices[t];
|
||||
var voice = selectedVoice;
|
||||
|
||||
if (voice.PlayerType == "zetvideo-vod" || voice.PlayerType == "ashdi-vod")
|
||||
{
|
||||
// Для zetvideo-vod та ashdi-vod використовуємо URL епізоду для виклику
|
||||
// Потрібно передати URL епізоду в інший параметр, щоб не плутати з play=true
|
||||
string callUrl = $"{host}/uaflix?episode_url={HttpUtility.UrlEncode(ep.File)}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&s={s}&e={ep.Number}";
|
||||
string callUrl = $"{host}/lite/uaflix?episode_url={HttpUtility.UrlEncode(ep.File)}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&s={s}&e={ep.Number}";
|
||||
episode_tpl.Append(
|
||||
name: ep.Title,
|
||||
name: episodeTitle,
|
||||
title: title,
|
||||
s: s.ToString(),
|
||||
e: ep.Number.ToString(),
|
||||
@ -350,16 +292,25 @@ namespace Uaflix.Controllers
|
||||
// Для багатосерійних плеєрів (ashdi-serial, zetvideo-serial) - пряме відтворення
|
||||
string playUrl = BuildStreamUrl(init, ep.File);
|
||||
episode_tpl.Append(
|
||||
name: ep.Title,
|
||||
name: episodeTitle,
|
||||
title: title,
|
||||
s: s.ToString(),
|
||||
e: ep.Number.ToString(),
|
||||
link: playUrl
|
||||
);
|
||||
}
|
||||
|
||||
appendedEpisodes++;
|
||||
}
|
||||
|
||||
OnLog($"Created EpisodeTpl with {episodes.Count} episodes");
|
||||
if (appendedEpisodes == 0)
|
||||
{
|
||||
OnLog($"No valid episodes after filtering for season {s}, voice {t}");
|
||||
OnLog("=== RETURN: no valid episodes OnError ===");
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
OnLog($"Created EpisodeTpl with {appendedEpisodes} episodes");
|
||||
|
||||
// Повертаємо VoiceTpl + EpisodeTpl разом
|
||||
episode_tpl.Append(voice_tpl);
|
||||
@ -378,7 +329,7 @@ namespace Uaflix.Controllers
|
||||
// Fallback: якщо жоден з умов не виконався
|
||||
OnLog($"Fallback: s={s}, t={t}");
|
||||
OnLog("=== RETURN: fallback OnError ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
else // Фільм
|
||||
{
|
||||
@ -386,7 +337,7 @@ namespace Uaflix.Controllers
|
||||
if (playResult?.streams == null || playResult.streams.Count == 0)
|
||||
{
|
||||
OnLog("=== RETURN: movie no streams ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var tpl = new MovieTpl(title, original_title, playResult.streams.Count);
|
||||
@ -407,7 +358,7 @@ namespace Uaflix.Controllers
|
||||
if (tpl.data == null || tpl.data.Count == 0)
|
||||
{
|
||||
OnLog("=== RETURN: movie template empty ===");
|
||||
return OnError("uaflix", proxyManager);
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
OnLog("=== RETURN: movie template ===");
|
||||
@ -435,6 +386,24 @@ namespace Uaflix.Controllers
|
||||
return HostStreamProxy(init, link);
|
||||
}
|
||||
|
||||
private void TryEnableMagicApn(OnlinesSettings init)
|
||||
{
|
||||
if (init == null
|
||||
|| init.apn != null
|
||||
|| init.streamproxy
|
||||
|| string.IsNullOrWhiteSpace(ModInit.MagicApnAshdiHost))
|
||||
return;
|
||||
|
||||
string player = new RchClient(HttpContext, host, init, requestInfo).InfoConnected()?.player;
|
||||
bool useInnerPlayer = string.IsNullOrWhiteSpace(player)
|
||||
|| player.Equals("inner", StringComparison.OrdinalIgnoreCase);
|
||||
if (!useInnerPlayer)
|
||||
return;
|
||||
|
||||
ApnHelper.ApplyInitConf(true, ModInit.MagicApnAshdiHost, init);
|
||||
OnLog($"Uaflix: увімкнено magic_apn для Ashdi (player={player ?? "unknown"}).");
|
||||
}
|
||||
|
||||
private static string StripLampacArgs(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
@ -451,23 +420,37 @@ namespace Uaflix.Controllers
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private static bool IsAshdiVoice(VoiceInfo voice)
|
||||
private static bool IsCheckOnlineSearchEnabled()
|
||||
{
|
||||
if (voice == null || string.IsNullOrEmpty(voice.PlayerType))
|
||||
return false;
|
||||
|
||||
return voice.PlayerType == "ashdi-serial" || voice.PlayerType == "ashdi-vod";
|
||||
}
|
||||
|
||||
private static HashSet<int> GetSeasonSet(VoiceInfo voice)
|
||||
try
|
||||
{
|
||||
if (voice?.Seasons == null || voice.Seasons.Count == 0)
|
||||
return new HashSet<int>();
|
||||
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.Seasons
|
||||
.Where(kv => kv.Value != null && kv.Value.Any(ep => !string.IsNullOrEmpty(ep.File)))
|
||||
.Select(kv => kv.Key)
|
||||
.ToHashSet();
|
||||
if (checkProp?.GetValue(conf) is bool enabled)
|
||||
return enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void OnLog(string message)
|
||||
{
|
||||
System.Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
Uaflix/GlobalUsings.cs
Normal file
4
Uaflix/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
global using Shared.Services;
|
||||
global using Shared.Services.Hybrid;
|
||||
global using Shared.Models.Base;
|
||||
global using AppInit = Shared.CoreInit;
|
||||
@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
@ -16,13 +17,14 @@ using Uaflix.Models;
|
||||
|
||||
namespace Uaflix
|
||||
{
|
||||
public class ModInit
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 4.0;
|
||||
public static double Version => 5.1;
|
||||
|
||||
public static UaflixSettings UaFlix;
|
||||
|
||||
public static bool ApnHostProvided;
|
||||
public static string MagicApnAshdiHost;
|
||||
|
||||
public static UaflixSettings Settings
|
||||
{
|
||||
@ -33,7 +35,7 @@ namespace Uaflix
|
||||
/// <summary>
|
||||
/// Модуль завантажено.
|
||||
/// </summary>
|
||||
public static void loaded(InitspaceModel initspace)
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
UaFlix = new UaflixSettings("Uaflix", "https://uafix.net", streamproxy: false, useproxy: false)
|
||||
{
|
||||
@ -53,8 +55,16 @@ namespace Uaflix
|
||||
}
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Conf("Uaflix", UaFlix) ?? JObject.FromObject(UaFlix);
|
||||
var defaults = JObject.FromObject(UaFlix);
|
||||
defaults["magic_apn"] = new JObject()
|
||||
{
|
||||
["ashdi"] = ApnHelper.DefaultHost
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Init("Uaflix", defaults) ?? defaults;
|
||||
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||
MagicApnAshdiHost = ApnHelper.TryGetMagicAshdiHost(conf);
|
||||
conf.Remove("magic_apn");
|
||||
conf.Remove("apn");
|
||||
conf.Remove("apn_host");
|
||||
UaFlix = conf.ToObject<UaflixSettings>();
|
||||
@ -62,8 +72,8 @@ namespace Uaflix
|
||||
if (hasApn)
|
||||
ApnHelper.ApplyInitConf(apnEnabled, apnHost, UaFlix);
|
||||
|
||||
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
|
||||
if (hasApn && apnEnabled)
|
||||
ApnHostProvided = ApnHelper.IsEnabled(UaFlix);
|
||||
if (ApnHostProvided)
|
||||
{
|
||||
UaFlix.streamproxy = false;
|
||||
}
|
||||
@ -74,7 +84,45 @@ namespace Uaflix
|
||||
}
|
||||
|
||||
// Показувати «уточнити пошук».
|
||||
AppInit.conf.online.with_search.Add("uaflix");
|
||||
RegisterWithSearch("uaflix");
|
||||
}
|
||||
|
||||
private static void RegisterWithSearch(string plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(plugin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,9 @@ namespace Uaflix.Models
|
||||
// Словник сезонів, де ключ - номер сезону, значення - кількість сторінок
|
||||
public Dictionary<int, int> Seasons { get; set; } = new Dictionary<int, int>();
|
||||
|
||||
// URL сторінки сезону: ключ - номер сезону, значення - абсолютний URL сторінки
|
||||
public Dictionary<int, string> SeasonUrls { get; set; } = new Dictionary<int, string>();
|
||||
|
||||
// Загальна кількість сторінок (якщо потрібно)
|
||||
public int TotalPages { get; set; }
|
||||
|
||||
|
||||
@ -1,37 +1,32 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Base;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Uaflix
|
||||
{
|
||||
public class OnlineApi
|
||||
public class OnlineApi : IModuleOnline
|
||||
{
|
||||
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||
HttpContext httpContext,
|
||||
IMemoryCache memoryCache,
|
||||
RequestModel requestInfo,
|
||||
string host,
|
||||
OnlineEventsModel args)
|
||||
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
|
||||
{
|
||||
long.TryParse(args.id, out long tmdbid);
|
||||
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||
}
|
||||
|
||||
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
private static List<ModuleOnlineItem> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
{
|
||||
var online = new List<(string name, string url, string plugin, int index)>();
|
||||
var online = new List<ModuleOnlineItem>();
|
||||
|
||||
var init = ModInit.UaFlix;
|
||||
if (init.enable && !init.rip)
|
||||
{
|
||||
string url = init.overridehost;
|
||||
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||
url = $"{host}/uaflix";
|
||||
if (UpdateService.IsDisconnected())
|
||||
init.overridehost = null;
|
||||
|
||||
online.Add((init.displayname, url, "uaflix", init.displayindex));
|
||||
online.Add(new ModuleOnlineItem(init, "uaflix"));
|
||||
}
|
||||
|
||||
return online;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -28,14 +28,16 @@ namespace Uaflix
|
||||
private readonly Action<string> _onLog;
|
||||
private readonly ProxyManager _proxyManager;
|
||||
private readonly UaflixAuth _auth;
|
||||
private readonly HttpHydra _httpHydra;
|
||||
|
||||
public UaflixInvoke(UaflixSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, UaflixAuth auth)
|
||||
public UaflixInvoke(UaflixSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, UaflixAuth auth, HttpHydra httpHydra = null)
|
||||
{
|
||||
_init = init;
|
||||
_hybridCache = hybridCache;
|
||||
_onLog = onLog;
|
||||
_proxyManager = proxyManager;
|
||||
_auth = auth;
|
||||
_httpHydra = httpHydra;
|
||||
}
|
||||
|
||||
string AshdiRequestUrl(string url)
|
||||
@ -76,7 +78,6 @@ namespace Uaflix
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return null;
|
||||
|
||||
string requestUrl = _init.cors(url);
|
||||
bool withAuth = ShouldUseAuth(url);
|
||||
var requestHeaders = headers != null ? new List<HeadersModel>(headers) : new List<HeadersModel>();
|
||||
|
||||
@ -86,6 +87,26 @@ namespace Uaflix
|
||||
_auth.ApplyCookieHeader(requestHeaders, cookie);
|
||||
}
|
||||
|
||||
if (_httpHydra != null)
|
||||
{
|
||||
string content = await _httpHydra.Get(url, newheaders: requestHeaders, statusCodeOK: false);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(content)
|
||||
&& retryOnUnauthorized
|
||||
&& withAuth
|
||||
&& _auth != null
|
||||
&& _auth.CanUseCredentials)
|
||||
{
|
||||
_onLog($"UaflixAuth: порожня відповідь для {url}, виконую повторну авторизацію");
|
||||
string refreshedCookie = await _auth.GetCookieHeaderAsync(forceRefresh: true);
|
||||
_auth.ApplyCookieHeader(requestHeaders, refreshedCookie);
|
||||
content = await _httpHydra.Get(url, newheaders: requestHeaders, statusCodeOK: false);
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(content) ? null : content;
|
||||
}
|
||||
|
||||
string requestUrl = _init.cors(url);
|
||||
var response = await Http.BaseGet(requestUrl,
|
||||
headers: requestHeaders,
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
@ -800,6 +821,440 @@ namespace Uaflix
|
||||
|
||||
#endregion
|
||||
|
||||
#region Сезонний (лінивий) парсинг серіалу
|
||||
|
||||
public async Task<PaginationInfo> GetSeasonIndex(string serialUrl)
|
||||
{
|
||||
string memKey = $"UaFlix:season-index:{serialUrl}";
|
||||
if (_hybridCache.TryGetValue(memKey, out PaginationInfo cached))
|
||||
return cached;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serialUrl) || !Uri.IsWellFormedUriString(serialUrl, UriKind.Absolute))
|
||||
return null;
|
||||
|
||||
var headers = new List<HeadersModel>()
|
||||
{
|
||||
new HeadersModel("User-Agent", "Mozilla/5.0"),
|
||||
new HeadersModel("Referer", _init.host)
|
||||
};
|
||||
|
||||
string html = await GetHtml(serialUrl, headers);
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
return null;
|
||||
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(html);
|
||||
|
||||
var result = new PaginationInfo
|
||||
{
|
||||
SerialUrl = serialUrl
|
||||
};
|
||||
|
||||
var seasonNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'sez-wr')]//a");
|
||||
if (seasonNodes == null)
|
||||
seasonNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'fss-box')]//a");
|
||||
|
||||
if (seasonNodes == null || seasonNodes.Count == 0)
|
||||
{
|
||||
// Якщо явного списку сезонів немає, вважаємо що є один сезон.
|
||||
result.Seasons[1] = 1;
|
||||
result.SeasonUrls[1] = serialUrl;
|
||||
_hybridCache.Set(memKey, result, cacheTime(40));
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var node in seasonNodes)
|
||||
{
|
||||
string href = node.GetAttributeValue("href", null);
|
||||
string seasonUrl = ToAbsoluteUrl(href);
|
||||
if (string.IsNullOrWhiteSpace(seasonUrl))
|
||||
continue;
|
||||
|
||||
string tabText = WebUtility.HtmlDecode(node.InnerText ?? string.Empty);
|
||||
if (!IsSeasonTabLink(seasonUrl, tabText))
|
||||
continue;
|
||||
|
||||
int season = ExtractSeasonNumber(seasonUrl, tabText);
|
||||
if (season <= 0)
|
||||
continue;
|
||||
|
||||
if (!result.SeasonUrls.TryGetValue(season, out string existing))
|
||||
{
|
||||
result.SeasonUrls[season] = seasonUrl;
|
||||
result.Seasons[season] = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsPreferableSeasonUrl(existing, seasonUrl, season))
|
||||
result.SeasonUrls[season] = seasonUrl;
|
||||
}
|
||||
|
||||
if (result.SeasonUrls.Count == 0)
|
||||
{
|
||||
result.Seasons[1] = 1;
|
||||
result.SeasonUrls[1] = serialUrl;
|
||||
}
|
||||
|
||||
_hybridCache.Set(memKey, result, cacheTime(40));
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onLog($"GetSeasonIndex error: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<EpisodeLinkInfo>> GetSeasonEpisodes(string serialUrl, int season)
|
||||
{
|
||||
if (season < 0)
|
||||
return new List<EpisodeLinkInfo>();
|
||||
|
||||
string memKey = $"UaFlix:season-episodes:{serialUrl}:{season}";
|
||||
if (_hybridCache.TryGetValue(memKey, out List<EpisodeLinkInfo> cached))
|
||||
return cached;
|
||||
|
||||
try
|
||||
{
|
||||
var headers = new List<HeadersModel>()
|
||||
{
|
||||
new HeadersModel("User-Agent", "Mozilla/5.0"),
|
||||
new HeadersModel("Referer", _init.host)
|
||||
};
|
||||
|
||||
var index = await GetSeasonIndex(serialUrl);
|
||||
string seasonUrl = index?.SeasonUrls != null && index.SeasonUrls.TryGetValue(season, out string mapped)
|
||||
? mapped
|
||||
: serialUrl;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(seasonUrl))
|
||||
seasonUrl = serialUrl;
|
||||
|
||||
string html = await GetHtml(seasonUrl, headers);
|
||||
if (string.IsNullOrWhiteSpace(html) && !string.Equals(seasonUrl, serialUrl, StringComparison.OrdinalIgnoreCase))
|
||||
html = await GetHtml(serialUrl, headers);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
return new List<EpisodeLinkInfo>();
|
||||
|
||||
var result = ParseSeasonEpisodesFromHtml(html, season);
|
||||
if (result.Count == 0 && !string.Equals(seasonUrl, serialUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string serialHtml = await GetHtml(serialUrl, headers);
|
||||
if (!string.IsNullOrWhiteSpace(serialHtml))
|
||||
result = ParseSeasonEpisodesFromHtml(serialHtml, season);
|
||||
}
|
||||
|
||||
if (result.Count == 0 && season == 1 && string.Equals(seasonUrl, serialUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Fallback для сторінок без окремих епізодів.
|
||||
result.Add(new EpisodeLinkInfo
|
||||
{
|
||||
url = serialUrl,
|
||||
title = "Епізод 1",
|
||||
season = 1,
|
||||
episode = 1
|
||||
});
|
||||
}
|
||||
|
||||
_hybridCache.Set(memKey, result, cacheTime(20));
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onLog($"GetSeasonEpisodes error: {ex.Message}");
|
||||
return new List<EpisodeLinkInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
List<EpisodeLinkInfo> ParseSeasonEpisodesFromHtml(string html, int season)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
return new List<EpisodeLinkInfo>();
|
||||
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(html);
|
||||
|
||||
var episodeNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'frels')]//a[contains(@class, 'vi-img')]");
|
||||
if (episodeNodes == null || episodeNodes.Count == 0)
|
||||
return new List<EpisodeLinkInfo>();
|
||||
|
||||
var episodes = new List<EpisodeLinkInfo>();
|
||||
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
int fallbackEpisode = 1;
|
||||
|
||||
foreach (var episodeNode in episodeNodes)
|
||||
{
|
||||
string episodeUrl = ToAbsoluteUrl(episodeNode.GetAttributeValue("href", null));
|
||||
if (string.IsNullOrWhiteSpace(episodeUrl) || !used.Add(episodeUrl))
|
||||
continue;
|
||||
|
||||
var match = Regex.Match(episodeUrl, @"season-(\d+).*?episode-(\d+)", RegexOptions.IgnoreCase);
|
||||
int parsedSeason = season;
|
||||
int parsedEpisode = fallbackEpisode;
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
if (int.TryParse(match.Groups[1].Value, out int seasonFromUrl))
|
||||
parsedSeason = seasonFromUrl;
|
||||
if (int.TryParse(match.Groups[2].Value, out int episodeFromUrl))
|
||||
parsedEpisode = episodeFromUrl;
|
||||
}
|
||||
|
||||
episodes.Add(new EpisodeLinkInfo
|
||||
{
|
||||
url = episodeUrl,
|
||||
title = episodeNode.SelectSingleNode(".//div[@class='vi-rate']")?.InnerText.Trim() ?? $"Епізод {parsedEpisode}",
|
||||
season = parsedSeason,
|
||||
episode = parsedEpisode
|
||||
});
|
||||
|
||||
fallbackEpisode = Math.Max(fallbackEpisode, parsedEpisode + 1);
|
||||
}
|
||||
|
||||
return episodes
|
||||
.Where(e => e != null && !string.IsNullOrWhiteSpace(e.url))
|
||||
.Where(e => e.season == season)
|
||||
.OrderBy(e => e.episode)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<SerialAggregatedStructure> GetSeasonStructure(string serialUrl, int season)
|
||||
{
|
||||
if (season < 0)
|
||||
return null;
|
||||
|
||||
string memKey = $"UaFlix:season-structure:{serialUrl}:{season}";
|
||||
if (_hybridCache.TryGetValue(memKey, out SerialAggregatedStructure cached))
|
||||
{
|
||||
_onLog($"GetSeasonStructure: Using cached structure for season={season}, url={serialUrl}");
|
||||
return cached;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var seasonEpisodes = await GetSeasonEpisodes(serialUrl, season);
|
||||
if (seasonEpisodes == null || seasonEpisodes.Count == 0)
|
||||
{
|
||||
_onLog($"GetSeasonStructure: No episodes for season={season}, url={serialUrl}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var structure = new SerialAggregatedStructure
|
||||
{
|
||||
SerialUrl = serialUrl,
|
||||
AllEpisodes = seasonEpisodes
|
||||
};
|
||||
|
||||
var seasonProbe = await ProbeSeasonPlayer(seasonEpisodes);
|
||||
if (string.IsNullOrWhiteSpace(seasonProbe.playerType))
|
||||
{
|
||||
// fallback: інколи плеєр є лише на головній сторінці
|
||||
seasonProbe = await ProbeEpisodePlayer(serialUrl);
|
||||
if (string.IsNullOrWhiteSpace(seasonProbe.playerType))
|
||||
{
|
||||
_onLog($"GetSeasonStructure: unsupported player for season={season}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (seasonProbe.playerType == "ashdi-serial" || seasonProbe.playerType == "zetvideo-serial")
|
||||
{
|
||||
var voices = await ParseMultiEpisodePlayerCached(seasonProbe.iframeUrl, seasonProbe.playerType);
|
||||
foreach (var voice in voices)
|
||||
{
|
||||
if (voice?.Seasons == null || !voice.Seasons.TryGetValue(season, out List<EpisodeInfo> seasonVoiceEpisodes) || seasonVoiceEpisodes == null || seasonVoiceEpisodes.Count == 0)
|
||||
continue;
|
||||
|
||||
structure.Voices[voice.DisplayName] = new VoiceInfo
|
||||
{
|
||||
Name = voice.Name,
|
||||
PlayerType = voice.PlayerType,
|
||||
DisplayName = voice.DisplayName,
|
||||
Seasons = new Dictionary<int, List<EpisodeInfo>>
|
||||
{
|
||||
[season] = seasonVoiceEpisodes
|
||||
.Where(ep => ep != null && !string.IsNullOrWhiteSpace(ep.File))
|
||||
.Select(ep => new EpisodeInfo
|
||||
{
|
||||
Number = ep.Number,
|
||||
Title = ep.Title,
|
||||
File = ep.File,
|
||||
Id = ep.Id,
|
||||
Poster = ep.Poster,
|
||||
Subtitle = ep.Subtitle
|
||||
})
|
||||
.ToList()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (seasonProbe.playerType == "ashdi-vod" || seasonProbe.playerType == "zetvideo-vod")
|
||||
{
|
||||
AddVodSeasonEpisodes(structure, seasonProbe.playerType, season, seasonEpisodes);
|
||||
}
|
||||
else
|
||||
{
|
||||
_onLog($"GetSeasonStructure: player '{seasonProbe.playerType}' is not supported");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!structure.Voices.Any())
|
||||
{
|
||||
_onLog($"GetSeasonStructure: voices are empty for season={season}, url={serialUrl}");
|
||||
return null;
|
||||
}
|
||||
|
||||
NormalizeUaflixVoiceNames(structure);
|
||||
_hybridCache.Set(memKey, structure, cacheTime(30));
|
||||
return structure;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onLog($"GetSeasonStructure error: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async Task<List<VoiceInfo>> ParseMultiEpisodePlayerCached(string iframeUrl, string playerType)
|
||||
{
|
||||
string serialKey = NormalizeSerialPlayerKey(playerType, iframeUrl);
|
||||
string memKey = $"UaFlix:player-voices:{playerType}:{serialKey}";
|
||||
if (_hybridCache.TryGetValue(memKey, out List<VoiceInfo> cached))
|
||||
return CloneVoices(cached);
|
||||
|
||||
var parsed = await ParseMultiEpisodePlayer(iframeUrl, playerType);
|
||||
if (parsed == null || parsed.Count == 0)
|
||||
return new List<VoiceInfo>();
|
||||
|
||||
_hybridCache.Set(memKey, parsed, cacheTime(40));
|
||||
return CloneVoices(parsed);
|
||||
}
|
||||
|
||||
static List<VoiceInfo> CloneVoices(List<VoiceInfo> voices)
|
||||
{
|
||||
if (voices == null || voices.Count == 0)
|
||||
return new List<VoiceInfo>();
|
||||
|
||||
var result = new List<VoiceInfo>(voices.Count);
|
||||
foreach (var voice in voices)
|
||||
{
|
||||
if (voice == null)
|
||||
continue;
|
||||
|
||||
var clone = new VoiceInfo
|
||||
{
|
||||
Name = voice.Name,
|
||||
PlayerType = voice.PlayerType,
|
||||
DisplayName = voice.DisplayName,
|
||||
Seasons = new Dictionary<int, List<EpisodeInfo>>()
|
||||
};
|
||||
|
||||
if (voice.Seasons != null)
|
||||
{
|
||||
foreach (var season in voice.Seasons)
|
||||
{
|
||||
clone.Seasons[season.Key] = season.Value?
|
||||
.Where(ep => ep != null)
|
||||
.Select(ep => new EpisodeInfo
|
||||
{
|
||||
Number = ep.Number,
|
||||
Title = ep.Title,
|
||||
File = ep.File,
|
||||
Id = ep.Id,
|
||||
Poster = ep.Poster,
|
||||
Subtitle = ep.Subtitle
|
||||
})
|
||||
.ToList() ?? new List<EpisodeInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(clone);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
string ToAbsoluteUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return null;
|
||||
|
||||
string clean = WebUtility.HtmlDecode(url.Trim());
|
||||
if (clean.StartsWith("//"))
|
||||
clean = "https:" + clean;
|
||||
|
||||
if (clean.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || clean.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
return clean;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_init?.host))
|
||||
return clean;
|
||||
|
||||
return $"{_init.host.TrimEnd('/')}/{clean.TrimStart('/')}";
|
||||
}
|
||||
|
||||
static bool IsSeasonTabLink(string url, string text)
|
||||
{
|
||||
string u = (url ?? string.Empty).ToLowerInvariant();
|
||||
string t = (text ?? string.Empty).ToLowerInvariant();
|
||||
|
||||
if (u.Contains("/date/") || t.Contains("графік") || t.Contains("дата виходу"))
|
||||
return false;
|
||||
|
||||
if (Regex.IsMatch(u, @"(?:sezon|season)[-_/ ]?\d+", RegexOptions.IgnoreCase))
|
||||
return true;
|
||||
|
||||
if (Regex.IsMatch(t, @"(?:сезон|season)\s*\d+", RegexOptions.IgnoreCase))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsPreferableSeasonUrl(string oldUrl, string newUrl, int season)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newUrl))
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(oldUrl))
|
||||
return true;
|
||||
|
||||
string marker = $"/sezon-{season}/";
|
||||
bool oldHasMarker = oldUrl.IndexOf(marker, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
bool newHasMarker = newUrl.IndexOf(marker, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
if (!oldHasMarker && newHasMarker)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static int ExtractSeasonNumber(string url, string text)
|
||||
{
|
||||
foreach (string source in new[] { url, text })
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
continue;
|
||||
|
||||
var seasonBySlug = Regex.Match(source, @"(?:sezon|season)[-_/ ]?(\d+)", RegexOptions.IgnoreCase);
|
||||
if (seasonBySlug.Success && int.TryParse(seasonBySlug.Groups[1].Value, out int seasonSlug) && seasonSlug > 0)
|
||||
return seasonSlug;
|
||||
|
||||
var seasonByWordUa = Regex.Match(source, @"сезон\s*(\d+)", RegexOptions.IgnoreCase);
|
||||
if (seasonByWordUa.Success && int.TryParse(seasonByWordUa.Groups[1].Value, out int seasonWordUa) && seasonWordUa > 0)
|
||||
return seasonWordUa;
|
||||
|
||||
var seasonByWordEn = Regex.Match(source, @"season\s*(\d+)", RegexOptions.IgnoreCase);
|
||||
if (seasonByWordEn.Success && int.TryParse(seasonByWordEn.Groups[1].Value, out int seasonWordEn) && seasonWordEn > 0)
|
||||
return seasonWordEn;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public async Task<List<SearchResult>> Search(string imdb_id, long kinopoisk_id, string title, string original_title, int year, int serial, string original_language, string source, string search_query)
|
||||
{
|
||||
bool allowAnime = IsAnimeRequest(title, original_title, original_language, source);
|
||||
@ -1790,7 +2245,7 @@ namespace Uaflix
|
||||
if (init != null && init.rhub && rhub != -1)
|
||||
return TimeSpan.FromMinutes(rhub);
|
||||
|
||||
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
|
||||
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
|
||||
if (ctime > multiaccess)
|
||||
ctime = multiaccess;
|
||||
|
||||
@ -1805,9 +2260,9 @@ namespace Uaflix
|
||||
if (init != null && init.rhub && rhub != -1)
|
||||
return TimeSpan.FromMinutes(rhub);
|
||||
|
||||
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
|
||||
if (init != null && ctime > init.cache_time && init.cache_time > 0)
|
||||
ctime = init.cache_time;
|
||||
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
|
||||
if (ctime > multiaccess)
|
||||
ctime = multiaccess;
|
||||
|
||||
return TimeSpan.FromMinutes(ctime);
|
||||
}
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "Uaflix.ModInit",
|
||||
"online": "Uaflix.OnlineApi"
|
||||
"enable": true
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
using Shared.Engine;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
@ -22,20 +21,20 @@ namespace Unimay.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("unimay")]
|
||||
[Route("lite/unimay")]
|
||||
async public ValueTask<ActionResult> Index(string title, string original_title, string code, int serial = -1, int s = -1, int e = -1, bool play = false, bool rjson = false, bool checksearch = false)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.Unimay);
|
||||
if (await IsBadInitialization(init, rch: false))
|
||||
if (await IsRequestBlocked(rch: false))
|
||||
return badInitMsg;
|
||||
|
||||
var invoke = new UnimayInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
var init = this.init;
|
||||
var invoke = new UnimayInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
|
||||
if (checksearch)
|
||||
{
|
||||
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||
if (!IsCheckOnlineSearchEnabled())
|
||||
return OnError("unimay");
|
||||
|
||||
var searchResults = await invoke.Search(title, original_title, serial);
|
||||
@ -169,5 +168,38 @@ namespace Unimay.Controllers
|
||||
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private static bool IsCheckOnlineSearchEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (checkProp?.GetValue(conf) is bool enabled)
|
||||
return enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void OnLog(string message)
|
||||
{
|
||||
System.Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
Unimay/GlobalUsings.cs
Normal file
4
Unimay/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
global using Shared.Services;
|
||||
global using Shared.Services.Hybrid;
|
||||
global using Shared.Models.Base;
|
||||
global using AppInit = Shared.CoreInit;
|
||||
@ -1,20 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Shared;
|
||||
using Shared.Models.Online.Settings;
|
||||
using Shared.Models.Module;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Events;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Shared;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using Shared.Models.Online.Settings;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
@ -28,9 +18,9 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Unimay
|
||||
{
|
||||
public class ModInit
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 3.4;
|
||||
public static double Version => 4.0;
|
||||
|
||||
public static OnlinesSettings Unimay;
|
||||
|
||||
@ -43,7 +33,7 @@ namespace Unimay
|
||||
/// <summary>
|
||||
/// модуль загружен
|
||||
/// </summary>
|
||||
public static void loaded(InitspaceModel initspace)
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
|
||||
|
||||
@ -59,10 +49,48 @@ namespace Unimay
|
||||
list = new string[] { "socks5://IP:PORT" }
|
||||
}
|
||||
};
|
||||
Unimay = ModuleInvoke.Conf("Unimay", Unimay).ToObject<OnlinesSettings>();
|
||||
Unimay = ModuleInvoke.Init("Unimay", JObject.FromObject(Unimay)).ToObject<OnlinesSettings>();
|
||||
|
||||
// Виводити "уточнити пошук"
|
||||
AppInit.conf.online.with_search.Add("unimay");
|
||||
RegisterWithSearch("unimay");
|
||||
}
|
||||
|
||||
private static void RegisterWithSearch(string plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onlineType = Type.GetType("Online.ModInit");
|
||||
if (onlineType == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
onlineType = asm.GetType("Online.ModInit");
|
||||
if (onlineType != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var conf = confField?.GetValue(null);
|
||||
var withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(plugin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,47 +1,36 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Base;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Unimay
|
||||
{
|
||||
public class OnlineApi
|
||||
public class OnlineApi : IModuleOnline
|
||||
{
|
||||
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||
HttpContext httpContext,
|
||||
IMemoryCache memoryCache,
|
||||
RequestModel requestInfo,
|
||||
string host,
|
||||
OnlineEventsModel args)
|
||||
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
|
||||
{
|
||||
long.TryParse(args.id, out long tmdbid);
|
||||
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||
}
|
||||
|
||||
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
private static List<ModuleOnlineItem> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||
{
|
||||
var online = new List<(string name, string url, string plugin, int index)>();
|
||||
var online = new List<ModuleOnlineItem>();
|
||||
|
||||
var init = ModInit.Unimay;
|
||||
|
||||
// Визначення isAnime згідно стандарту Lampac (Deepwiki):
|
||||
// isanime = true якщо original_language == "ja" або "zh"
|
||||
bool hasLang = !string.IsNullOrEmpty(original_language);
|
||||
bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
|
||||
|
||||
// Unimay — аніме-провайдер. Додаємо якщо:
|
||||
// - загальний пошук (serial == -1), або
|
||||
// - контент є аніме (isanime), або
|
||||
// - мова невідома (немає original_language)
|
||||
if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang))
|
||||
{
|
||||
string url = init.overridehost;
|
||||
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||
url = $"{host}/unimay";
|
||||
if (UpdateService.IsDisconnected())
|
||||
init.overridehost = null;
|
||||
|
||||
online.Add((init.displayname, url, "unimay", init.displayindex));
|
||||
online.Add(new ModuleOnlineItem(init, "unimay"));
|
||||
}
|
||||
|
||||
return online;
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -6,7 +6,6 @@ using Shared.Models.Online.Settings;
|
||||
using Shared.Models;
|
||||
using System.Linq;
|
||||
using Unimay.Models;
|
||||
using Shared.Engine;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
@ -18,13 +17,15 @@ namespace Unimay
|
||||
private ProxyManager _proxyManager;
|
||||
private IHybridCache _hybridCache;
|
||||
private Action<string> _onLog;
|
||||
private readonly HttpHydra _httpHydra;
|
||||
|
||||
public UnimayInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||
public UnimayInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
|
||||
{
|
||||
_init = init;
|
||||
_hybridCache = hybridCache;
|
||||
_onLog = onLog;
|
||||
_proxyManager = proxyManager;
|
||||
_httpHydra = httpHydra;
|
||||
}
|
||||
|
||||
public async Task<SearchResponse> Search(string title, string original_title, int serial)
|
||||
@ -39,7 +40,7 @@ namespace Unimay
|
||||
string searchUrl = $"{_init.host}/release/search?page=0&page_size=10&title={searchQuery}";
|
||||
|
||||
var headers = httpHeaders(_init);
|
||||
SearchResponse root = await Http.Get<SearchResponse>(_init.cors(searchUrl), timeoutSeconds: 8, proxy: _proxyManager.Get(), headers: headers);
|
||||
SearchResponse root = await HttpGet<SearchResponse>(searchUrl, headers, timeoutSeconds: 8);
|
||||
|
||||
if (root == null || root.Content == null || root.Content.Count == 0)
|
||||
{
|
||||
@ -69,7 +70,7 @@ namespace Unimay
|
||||
string releaseUrl = $"{_init.host}/release?code={code}";
|
||||
|
||||
var headers = httpHeaders(_init);
|
||||
ReleaseResponse root = await Http.Get<ReleaseResponse>(_init.cors(releaseUrl), timeoutSeconds: 8, proxy: _proxyManager.Get(), headers: headers);
|
||||
ReleaseResponse root = await HttpGet<ReleaseResponse>(releaseUrl, headers, timeoutSeconds: 8);
|
||||
|
||||
if (root == null)
|
||||
{
|
||||
@ -103,7 +104,7 @@ namespace Unimay
|
||||
}
|
||||
|
||||
string itemTitle = item.Names?.Ukr ?? item.Names?.Eng ?? item.Title;
|
||||
string releaseUrl = $"{host}/unimay?code={item.Code}&title={System.Web.HttpUtility.UrlEncode(itemTitle)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial={serial}";
|
||||
string releaseUrl = $"{host}/lite/unimay?code={item.Code}&title={System.Web.HttpUtility.UrlEncode(itemTitle)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial={serial}";
|
||||
results.Add((itemTitle, item.Year, item.Type, releaseUrl));
|
||||
}
|
||||
|
||||
@ -116,7 +117,7 @@ namespace Unimay
|
||||
return (null, null);
|
||||
|
||||
var movieEpisode = releaseDetail.Playlist[0];
|
||||
string movieLink = $"{host}/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=0&play=true";
|
||||
string movieLink = $"{host}/lite/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=0&play=true";
|
||||
string movieTitle = movieEpisode.Title ?? title;
|
||||
|
||||
return (movieTitle, movieLink);
|
||||
@ -124,7 +125,7 @@ namespace Unimay
|
||||
|
||||
public (string seasonName, string seasonUrl, string seasonId) GetSeasonInfo(string host, string code, string title, string original_title)
|
||||
{
|
||||
string seasonUrl = $"{host}/unimay?code={code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1";
|
||||
string seasonUrl = $"{host}/lite/unimay?code={code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1";
|
||||
return ("Сезон 1", seasonUrl, "1");
|
||||
}
|
||||
|
||||
@ -138,7 +139,7 @@ namespace Unimay
|
||||
foreach (var ep in releaseDetail.Playlist.Where(ep => ep.Number >= 1 && ep.Number <= 24).OrderBy(ep => ep.Number))
|
||||
{
|
||||
string epTitle = ep.Title ?? $"Епізод {ep.Number}";
|
||||
string epLink = $"{host}/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1&e={ep.Number}&play=true";
|
||||
string epLink = $"{host}/lite/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1&e={ep.Number}&play=true";
|
||||
episodes.Add((epTitle, epLink));
|
||||
}
|
||||
|
||||
@ -160,12 +161,20 @@ namespace Unimay
|
||||
};
|
||||
}
|
||||
|
||||
private Task<T> HttpGet<T>(string url, List<HeadersModel> headers, int timeoutSeconds = 15)
|
||||
{
|
||||
if (_httpHydra != null)
|
||||
return _httpHydra.Get<T>(url, newheaders: headers);
|
||||
|
||||
return Http.Get<T>(_init.cors(url), timeoutSeconds: timeoutSeconds, proxy: _proxyManager.Get(), headers: headers);
|
||||
}
|
||||
|
||||
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
|
||||
{
|
||||
if (init != null && init.rhub && rhub != -1)
|
||||
return TimeSpan.FromMinutes(rhub);
|
||||
|
||||
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
|
||||
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
|
||||
if (ctime > multiaccess)
|
||||
ctime = multiaccess;
|
||||
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "Unimay.ModInit",
|
||||
"online": "Unimay.OnlineApi"
|
||||
"enable": true
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user