mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-04-16 09:22:21 +00:00
commit
85849c78d2
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,4 +6,5 @@
|
|||||||
/.clinerules/moduls.md
|
/.clinerules/moduls.md
|
||||||
/.clinerules/uaflix-optimization.md
|
/.clinerules/uaflix-optimization.md
|
||||||
/.clinerules/
|
/.clinerules/
|
||||||
/.qodo/
|
/.qodo/
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@ -28,7 +28,7 @@ namespace AnimeON.Controllers
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("animeon")]
|
[Route("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)
|
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, bool rjson = false, bool checksearch = false)
|
||||||
{
|
{
|
||||||
await UpdateService.ConnectAsync(host);
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
@ -37,6 +37,19 @@ namespace AnimeON.Controllers
|
|||||||
return Forbid();
|
return Forbid();
|
||||||
|
|
||||||
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager);
|
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
|
||||||
|
if (checksearch)
|
||||||
|
{
|
||||||
|
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||||
|
return OnError("animeon", proxyManager);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
OnLog($"AnimeON Index: title={title}, original_title={original_title}, serial={serial}, s={s}, t={t}, year={year}, imdb_id={imdb_id}, kp={kinopoisk_id}");
|
OnLog($"AnimeON Index: title={title}, original_title={original_title}, serial={serial}, s={s}, t={t}, year={year}, imdb_id={imdb_id}, kp={kinopoisk_id}");
|
||||||
|
|
||||||
var seasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial);
|
var seasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial);
|
||||||
@ -161,7 +174,7 @@ namespace AnimeON.Controllers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string playUrl = HostStreamProxy(init, accsArgs(streamLink));
|
string playUrl = BuildStreamUrl(init, streamLink, headers: null, forceProxy: false);
|
||||||
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, playUrl);
|
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, playUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,7 +230,7 @@ namespace AnimeON.Controllers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
tpl.Append(translationName, HostStreamProxy(init, accsArgs(streamLink)));
|
tpl.Append(translationName, BuildStreamUrl(init, streamLink, headers: null, forceProxy: false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -376,9 +389,30 @@ namespace AnimeON.Controllers
|
|||||||
return UpdateService.Validate(Content(jsonResult, "application/json; charset=utf-8"));
|
return UpdateService.Validate(Content(jsonResult, "application/json; charset=utf-8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
string BuildStreamUrl(OnlinesSettings init, string streamLink, List<HeadersModel> headers, bool forceProxy)
|
string BuildStreamUrl(OnlinesSettings init, string streamLink, List<HeadersModel> headers, bool forceProxy)
|
||||||
{
|
{
|
||||||
string link = accsArgs(streamLink);
|
string link = streamLink?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(link))
|
||||||
|
return link;
|
||||||
|
|
||||||
|
link = StripLampacArgs(link);
|
||||||
|
|
||||||
if (ApnHelper.IsEnabled(init))
|
if (ApnHelper.IsEnabled(init))
|
||||||
{
|
{
|
||||||
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
||||||
|
|||||||
@ -25,7 +25,7 @@ namespace AnimeON
|
|||||||
{
|
{
|
||||||
public class ModInit
|
public class ModInit
|
||||||
{
|
{
|
||||||
public static double Version => 3.3;
|
public static double Version => 3.4;
|
||||||
|
|
||||||
public static OnlinesSettings AnimeON;
|
public static OnlinesSettings AnimeON;
|
||||||
public static bool ApnHostProvided;
|
public static bool ApnHostProvided;
|
||||||
|
|||||||
@ -24,7 +24,7 @@ namespace Bamboo.Controllers
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("bamboo")]
|
[Route("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)
|
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, bool rjson = false, string href = null, bool checksearch = false)
|
||||||
{
|
{
|
||||||
await UpdateService.ConnectAsync(host);
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
@ -34,6 +34,18 @@ namespace Bamboo.Controllers
|
|||||||
|
|
||||||
var invoke = new BambooInvoke(init, hybridCache, OnLog, proxyManager);
|
var invoke = new BambooInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
|
||||||
|
if (checksearch)
|
||||||
|
{
|
||||||
|
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||||
|
return OnError("bamboo", proxyManager);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
string itemUrl = href;
|
string itemUrl = href;
|
||||||
if (string.IsNullOrEmpty(itemUrl))
|
if (string.IsNullOrEmpty(itemUrl))
|
||||||
{
|
{
|
||||||
@ -121,7 +133,10 @@ namespace Bamboo.Controllers
|
|||||||
|
|
||||||
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
||||||
{
|
{
|
||||||
string link = accsArgs(streamLink);
|
string link = StripLampacArgs(streamLink?.Trim());
|
||||||
|
if (string.IsNullOrEmpty(link))
|
||||||
|
return link;
|
||||||
|
|
||||||
if (ApnHelper.IsEnabled(init))
|
if (ApnHelper.IsEnabled(init))
|
||||||
{
|
{
|
||||||
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
||||||
@ -135,5 +150,21 @@ namespace Bamboo.Controllers
|
|||||||
|
|
||||||
return HostStreamProxy(init, link);
|
return HostStreamProxy(init, link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ namespace Bamboo
|
|||||||
{
|
{
|
||||||
public class ModInit
|
public class ModInit
|
||||||
{
|
{
|
||||||
public static double Version => 3.4;
|
public static double Version => 3.5;
|
||||||
|
|
||||||
public static OnlinesSettings Bamboo;
|
public static OnlinesSettings Bamboo;
|
||||||
public static bool ApnHostProvided;
|
public static bool ApnHostProvided;
|
||||||
|
|||||||
@ -27,7 +27,7 @@ namespace CikavaIdeya.Controllers
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("cikavaideya")]
|
[Route("cikavaideya")]
|
||||||
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)
|
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, bool checksearch = false)
|
||||||
{
|
{
|
||||||
await UpdateService.ConnectAsync(host);
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
@ -37,6 +37,18 @@ namespace CikavaIdeya.Controllers
|
|||||||
|
|
||||||
var invoke = new CikavaIdeyaInvoke(init, hybridCache, OnLog, proxyManager);
|
var invoke = new CikavaIdeyaInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
|
||||||
|
if (checksearch)
|
||||||
|
{
|
||||||
|
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||||
|
return OnError("cikavaideya", proxyManager);
|
||||||
|
|
||||||
|
var checkEpisodes = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial == 0);
|
||||||
|
if (checkEpisodes != null && checkEpisodes.Count > 0)
|
||||||
|
return Content("data-json=", "text/plain; charset=utf-8");
|
||||||
|
|
||||||
|
return OnError("cikavaideya", proxyManager);
|
||||||
|
}
|
||||||
|
|
||||||
var episodesInfo = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial == 0);
|
var episodesInfo = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial == 0);
|
||||||
if (episodesInfo == null)
|
if (episodesInfo == null)
|
||||||
return Content("CikavaIdeya", "text/html; charset=utf-8");
|
return Content("CikavaIdeya", "text/html; charset=utf-8");
|
||||||
@ -388,7 +400,10 @@ namespace CikavaIdeya.Controllers
|
|||||||
|
|
||||||
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
||||||
{
|
{
|
||||||
string link = accsArgs(streamLink);
|
string link = StripLampacArgs(streamLink?.Trim());
|
||||||
|
if (string.IsNullOrEmpty(link))
|
||||||
|
return link;
|
||||||
|
|
||||||
if (ApnHelper.IsEnabled(init))
|
if (ApnHelper.IsEnabled(init))
|
||||||
{
|
{
|
||||||
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
||||||
@ -402,5 +417,21 @@ namespace CikavaIdeya.Controllers
|
|||||||
|
|
||||||
return HostStreamProxy(init, link);
|
return HostStreamProxy(init, link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ namespace CikavaIdeya
|
|||||||
{
|
{
|
||||||
public class ModInit
|
public class ModInit
|
||||||
{
|
{
|
||||||
public static double Version => 3.2;
|
public static double Version => 010100100100100101010000;
|
||||||
|
|
||||||
public static OnlinesSettings CikavaIdeya;
|
public static OnlinesSettings CikavaIdeya;
|
||||||
public static bool ApnHostProvided;
|
public static bool ApnHostProvided;
|
||||||
|
|||||||
98
Makhno/ApnHelper.cs
Normal file
98
Makhno/ApnHelper.cs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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 == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (apnToken.Type == JTokenType.Boolean)
|
||||||
|
{
|
||||||
|
enabled = apnToken.Value<bool>();
|
||||||
|
host = conf.Value<string>("apn_host");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apnToken.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
host = apnToken.Value<string>();
|
||||||
|
enabled = !string.IsNullOrWhiteSpace(host);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
|
||||||
|
{
|
||||||
|
if (init == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
init.apnstream = false;
|
||||||
|
init.apn = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(host))
|
||||||
|
host = DefaultHost;
|
||||||
|
|
||||||
|
if (init.apn == null)
|
||||||
|
init.apn = new ApnConf();
|
||||||
|
|
||||||
|
init.apn.host = host;
|
||||||
|
init.apnstream = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsEnabled(BaseSettings init)
|
||||||
|
{
|
||||||
|
return init?.apnstream == true && !string.IsNullOrWhiteSpace(init?.apn?.host);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsAshdiUrl(string url)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(url) &&
|
||||||
|
url.IndexOf("ashdi.vip", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string WrapUrl(BaseSettings init, string url)
|
||||||
|
{
|
||||||
|
if (!IsEnabled(init))
|
||||||
|
return url;
|
||||||
|
|
||||||
|
return BuildUrl(init.apn.host, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BuildUrl(string host, string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(url))
|
||||||
|
return url;
|
||||||
|
|
||||||
|
if (host.Contains("{encodeurl}"))
|
||||||
|
return host.Replace("{encodeurl}", HttpUtility.UrlEncode(url));
|
||||||
|
|
||||||
|
if (host.Contains("{encode_uri}"))
|
||||||
|
return host.Replace("{encode_uri}", HttpUtility.UrlEncode(url));
|
||||||
|
|
||||||
|
if (host.Contains("{uri}"))
|
||||||
|
return host.Replace("{uri}", url);
|
||||||
|
|
||||||
|
return $"{host.TrimEnd('/')}/{url}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
616
Makhno/Controller.cs
Normal file
616
Makhno/Controller.cs
Normal file
@ -0,0 +1,616 @@
|
|||||||
|
using Shared.Engine;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Web;
|
||||||
|
using Shared;
|
||||||
|
using Shared.Models.Templates;
|
||||||
|
using Shared.Models.Online.Settings;
|
||||||
|
using Shared.Models;
|
||||||
|
using Makhno.Models;
|
||||||
|
|
||||||
|
namespace Makhno
|
||||||
|
{
|
||||||
|
[Route("makhno")]
|
||||||
|
public class MakhnoController : BaseOnlineController
|
||||||
|
{
|
||||||
|
private readonly ProxyManager proxyManager;
|
||||||
|
|
||||||
|
public MakhnoController() : base(ModInit.Settings)
|
||||||
|
{
|
||||||
|
proxyManager = new ProxyManager(ModInit.Makhno);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
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, int season = -1, bool rjson = false, bool checksearch = false)
|
||||||
|
{
|
||||||
|
if (checksearch)
|
||||||
|
{
|
||||||
|
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
return Content("data-json=", "text/plain; charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
|
var init = await loadKit(ModInit.Makhno);
|
||||||
|
if (!init.enable)
|
||||||
|
return OnError();
|
||||||
|
Initialization(init);
|
||||||
|
|
||||||
|
OnLog($"Makhno: {title} (serial={serial}, s={s}, season={season}, t={t})");
|
||||||
|
|
||||||
|
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
|
||||||
|
var resolved = await ResolvePlaySource(imdb_id, title, original_title, year, serial, invoke);
|
||||||
|
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
if (resolved.ShouldEnrich)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await EnrichWormhole(imdb_id, title, original_title, year, resolved, invoke);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnLog($"Makhno wormhole enrich failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved.IsSerial)
|
||||||
|
return await HandleSerial(resolved.PlayUrl, imdb_id, title, original_title, year, t, season, rjson, invoke);
|
||||||
|
|
||||||
|
return await HandleMovie(resolved.PlayUrl, imdb_id, title, original_title, year, rjson, invoke);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("play")]
|
||||||
|
public async Task<ActionResult> Play(long id, string imdb_id, long kinopoisk_id, string title, string original_title, int year, int s, int season, string t, string episodeId, bool play = false, bool rjson = false)
|
||||||
|
{
|
||||||
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
|
var init = await loadKit(ModInit.Makhno);
|
||||||
|
if (!init.enable)
|
||||||
|
return OnError();
|
||||||
|
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 resolved = await ResolvePlaySource(imdb_id, title, original_title, year, serial: 1, invoke);
|
||||||
|
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
var playerData = await InvokeCache<PlayerData>($"makhno:player:{resolved.PlayUrl}", TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
return await invoke.GetPlayerData(resolved.PlayUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (playerData?.Voices == null || !playerData.Voices.Any())
|
||||||
|
{
|
||||||
|
OnLog("Makhno Play: no voices parsed");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(t) || !int.TryParse(t, out int voiceIndex) || voiceIndex >= playerData.Voices.Count)
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
var selectedVoice = playerData.Voices[voiceIndex];
|
||||||
|
int seasonIndex = season > 0 ? season - 1 : season;
|
||||||
|
if (seasonIndex < 0 || seasonIndex >= selectedVoice.Seasons.Count)
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
var selectedSeason = selectedVoice.Seasons[seasonIndex];
|
||||||
|
foreach (var episode in selectedSeason.Episodes)
|
||||||
|
{
|
||||||
|
if (episode.Id == episodeId && !string.IsNullOrEmpty(episode.File))
|
||||||
|
{
|
||||||
|
OnLog($"Makhno Play: Found episode {episode.Title}, stream: {episode.File}");
|
||||||
|
|
||||||
|
string streamUrl = BuildStreamUrl(init, episode.File);
|
||||||
|
string episodeTitle = $"{title ?? original_title} - {episode.Title}";
|
||||||
|
|
||||||
|
if (play)
|
||||||
|
return UpdateService.Validate(Redirect(streamUrl));
|
||||||
|
|
||||||
|
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, episodeTitle), "application/json; charset=utf-8"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnLog("Makhno Play: Episode not found");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("play/movie")]
|
||||||
|
public async Task<ActionResult> PlayMovie(long id, string imdb_id, string title, string original_title, int year, bool play = false, bool rjson = false)
|
||||||
|
{
|
||||||
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
|
var init = await loadKit(ModInit.Makhno);
|
||||||
|
if (!init.enable)
|
||||||
|
return OnError();
|
||||||
|
Initialization(init);
|
||||||
|
|
||||||
|
OnLog($"Makhno PlayMovie: {title} ({year}) play={play}");
|
||||||
|
|
||||||
|
var invoke = new MakhnoInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
var resolved = await ResolvePlaySource(imdb_id, title, original_title, year, serial: 0, invoke);
|
||||||
|
if (resolved == null || string.IsNullOrEmpty(resolved.PlayUrl))
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
var playerData = await InvokeCache<PlayerData>($"makhno:player:{resolved.PlayUrl}", TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
return await invoke.GetPlayerData(resolved.PlayUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (playerData?.File == null)
|
||||||
|
{
|
||||||
|
OnLog("Makhno PlayMovie: no file parsed");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
string streamUrl = BuildStreamUrl(init, playerData.File);
|
||||||
|
|
||||||
|
if (play)
|
||||||
|
return UpdateService.Validate(Redirect(streamUrl));
|
||||||
|
|
||||||
|
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? original_title), "application/json; charset=utf-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ActionResult> HandleMovie(string playUrl, string imdb_id, string title, string original_title, int year, bool rjson, MakhnoInvoke invoke)
|
||||||
|
{
|
||||||
|
var init = ModInit.Makhno;
|
||||||
|
var playerData = await InvokeCache<PlayerData>($"makhno:player:{playUrl}", TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
return await invoke.GetPlayerData(playUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (playerData?.File == null)
|
||||||
|
{
|
||||||
|
OnLog("Makhno HandleMovie: no file parsed");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
string movieLink = $"{host}/makhno/play/movie?imdb_id={HttpUtility.UrlEncode(imdb_id)}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&play=true";
|
||||||
|
var tpl = new MovieTpl(title ?? original_title, original_title, 1);
|
||||||
|
tpl.Append(title ?? original_title, accsArgs(movieLink), method: "play");
|
||||||
|
|
||||||
|
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ActionResult> HandleSerial(string playUrl, string imdb_id, string title, string original_title, int year, string t, int season, bool rjson, MakhnoInvoke invoke)
|
||||||
|
{
|
||||||
|
var init = ModInit.Makhno;
|
||||||
|
|
||||||
|
var playerData = await InvokeCache<PlayerData>($"makhno:player:{playUrl}", TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
return await invoke.GetPlayerData(playUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (playerData?.Voices == null || !playerData.Voices.Any())
|
||||||
|
{
|
||||||
|
OnLog("Makhno HandleSerial: no voices parsed");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
var voiceSeasons = playerData.Voices
|
||||||
|
.Select((voice, index) => new
|
||||||
|
{
|
||||||
|
Voice = voice,
|
||||||
|
Index = index,
|
||||||
|
Seasons = GetSeasonsWithNumbers(voice)
|
||||||
|
})
|
||||||
|
.Where(v => v.Seasons.Count > 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Debug logging disabled to avoid noisy output in production.
|
||||||
|
|
||||||
|
var seasonNumbers = voiceSeasons
|
||||||
|
.SelectMany(v => v.Seasons.Select(s => s.Number))
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(n => n)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (seasonNumbers.Count == 0)
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
if (season == -1)
|
||||||
|
{
|
||||||
|
int? seasonVoiceIndex = null;
|
||||||
|
if (int.TryParse(t, out int tIndex) && tIndex >= 0 && tIndex < playerData.Voices.Count)
|
||||||
|
seasonVoiceIndex = tIndex;
|
||||||
|
|
||||||
|
if (seasonVoiceIndex.HasValue)
|
||||||
|
{
|
||||||
|
var seasonsForVoice = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndex.Value])
|
||||||
|
.Select(s => s.Number)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(n => n)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (seasonsForVoice.Count > 0)
|
||||||
|
seasonNumbers = seasonsForVoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
var season_tpl = new SeasonTpl();
|
||||||
|
foreach (var seasonNumber in seasonNumbers)
|
||||||
|
{
|
||||||
|
(Season Season, int Number)? seasonItem = null;
|
||||||
|
if (seasonVoiceIndex.HasValue)
|
||||||
|
{
|
||||||
|
var voiceSeasonsForT = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndex.Value]);
|
||||||
|
var match = voiceSeasonsForT.FirstOrDefault(s => s.Number == seasonNumber);
|
||||||
|
seasonItem = match.Season != null ? match : ((Season Season, int Number)?)null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var match = voiceSeasons
|
||||||
|
.SelectMany(v => v.Seasons)
|
||||||
|
.FirstOrDefault(s => s.Number == seasonNumber);
|
||||||
|
seasonItem = match.Season != null ? match : ((Season Season, int Number)?)null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}";
|
||||||
|
season_tpl.Append(seasonName, link, seasonNumber.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
var voice_tpl = new VoiceTpl();
|
||||||
|
var episode_tpl = new EpisodeTpl();
|
||||||
|
|
||||||
|
int requestedSeason = seasonNumbers.Contains(season) ? season : seasonNumbers.First();
|
||||||
|
|
||||||
|
int? seasonVoiceIndexForTpl = null;
|
||||||
|
string selectedVoice = t;
|
||||||
|
if (string.IsNullOrEmpty(selectedVoice) || !int.TryParse(selectedVoice, out int selectedVoiceIndex))
|
||||||
|
{
|
||||||
|
var voiceWithSeason = voiceSeasons.FirstOrDefault(v => v.Seasons.Any(s => s.Number == requestedSeason));
|
||||||
|
selectedVoice = voiceWithSeason != null ? voiceWithSeason.Index.ToString() : voiceSeasons.First().Index.ToString();
|
||||||
|
}
|
||||||
|
else if (selectedVoiceIndex >= 0 && selectedVoiceIndex < playerData.Voices.Count)
|
||||||
|
{
|
||||||
|
seasonVoiceIndexForTpl = selectedVoiceIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<int> selectedVoiceSeasonSet = null;
|
||||||
|
if (seasonVoiceIndexForTpl.HasValue)
|
||||||
|
{
|
||||||
|
selectedVoiceSeasonSet = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndexForTpl.Value])
|
||||||
|
.Select(s => s.Number)
|
||||||
|
.ToHashSet();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
selectedVoiceSeasonSet = seasonNumbers.ToHashSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build season template for selected voice (if valid) to keep season list in sync when switching voices.
|
||||||
|
var seasonTplForVoice = new SeasonTpl();
|
||||||
|
List<int> seasonNumbersForTpl = seasonNumbers;
|
||||||
|
if (seasonVoiceIndexForTpl.HasValue)
|
||||||
|
{
|
||||||
|
var seasonsForVoiceTpl = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndexForTpl.Value])
|
||||||
|
.Select(s => s.Number)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(n => n)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (seasonsForVoiceTpl.Count > 0)
|
||||||
|
seasonNumbersForTpl = seasonsForVoiceTpl;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var seasonNumber in seasonNumbersForTpl)
|
||||||
|
{
|
||||||
|
(Season Season, int Number)? seasonItem = null;
|
||||||
|
if (seasonVoiceIndexForTpl.HasValue)
|
||||||
|
{
|
||||||
|
var voiceSeasonsForT = GetSeasonsWithNumbers(playerData.Voices[seasonVoiceIndexForTpl.Value]);
|
||||||
|
var match = voiceSeasonsForT.FirstOrDefault(s => s.Number == seasonNumber);
|
||||||
|
seasonItem = match.Season != null ? match : ((Season Season, int Number)?)null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var match = voiceSeasons
|
||||||
|
.SelectMany(v => v.Seasons)
|
||||||
|
.FirstOrDefault(s => s.Number == seasonNumber);
|
||||||
|
seasonItem = match.Season != null ? match : ((Season Season, int Number)?)null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}";
|
||||||
|
seasonTplForVoice.Append(seasonName, link, seasonNumber.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < playerData.Voices.Count; i++)
|
||||||
|
{
|
||||||
|
var voice = playerData.Voices[i];
|
||||||
|
string voiceName = voice.Name ?? $"Озвучка {i + 1}";
|
||||||
|
var seasonsForVoice = GetSeasonsWithNumbers(voice);
|
||||||
|
if (seasonsForVoice.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string voiceLink;
|
||||||
|
bool hasRequestedSeason = seasonsForVoice.Any(s => s.Number == requestedSeason);
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isActive = selectedVoice == i.ToString();
|
||||||
|
voice_tpl.Append(voiceName, isActive, voiceLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(selectedVoice) && int.TryParse(selectedVoice, out int voiceIndex) && voiceIndex < playerData.Voices.Count)
|
||||||
|
{
|
||||||
|
var selectedVoiceData = playerData.Voices[voiceIndex];
|
||||||
|
var seasonsForVoice = GetSeasonsWithNumbers(selectedVoiceData);
|
||||||
|
if (seasonsForVoice.Count > 0)
|
||||||
|
{
|
||||||
|
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}";
|
||||||
|
return UpdateService.Validate(Redirect(redirectUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedSeason = seasonsForVoice.First(s => s.Number == requestedSeason).Season;
|
||||||
|
var sortedEpisodes = selectedSeason.Episodes.OrderBy(e => ExtractEpisodeNumber(e.Title)).ToList();
|
||||||
|
|
||||||
|
for (int i = 0; i < sortedEpisodes.Count; i++)
|
||||||
|
{
|
||||||
|
var episode = sortedEpisodes[i];
|
||||||
|
if (!string.IsNullOrEmpty(episode.File))
|
||||||
|
{
|
||||||
|
string streamUrl = BuildStreamUrl(init, episode.File);
|
||||||
|
episode_tpl.Append(
|
||||||
|
episode.Title,
|
||||||
|
title ?? original_title,
|
||||||
|
requestedSeason.ToString(),
|
||||||
|
(i + 1).ToString("D2"),
|
||||||
|
streamUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
episode_tpl.Append(voice_tpl);
|
||||||
|
if (rjson)
|
||||||
|
return Content(episode_tpl.ToJson(), "application/json; charset=utf-8");
|
||||||
|
|
||||||
|
return Content(seasonTplForVoice.ToHtml() + episode_tpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ExtractEpisodeNumber(string title)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(title))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(title, @"(\d+)");
|
||||||
|
return match.Success ? int.Parse(match.Groups[1].Value) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int? ExtractSeasonNumber(string title)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(title))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(title, @"(\d+)");
|
||||||
|
return match.Success ? int.Parse(match.Groups[1].Value) : (int?)null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<(Season Season, int Number)> GetSeasonsWithNumbers(Voice voice)
|
||||||
|
{
|
||||||
|
var result = new List<(Season Season, int Number)>();
|
||||||
|
if (voice?.Seasons == null || voice.Seasons.Count == 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
for (int i = 0; i < voice.Seasons.Count; i++)
|
||||||
|
{
|
||||||
|
var season = voice.Seasons[i];
|
||||||
|
int number = ExtractSeasonNumber(season?.Title) ?? (i + 1);
|
||||||
|
result.Add((season, number));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ResolveResult> ResolvePlaySource(string imdbId, string title, string originalTitle, int year, int serial, MakhnoInvoke invoke)
|
||||||
|
{
|
||||||
|
string playUrl = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(imdbId))
|
||||||
|
{
|
||||||
|
string cacheKey = $"makhno:wormhole:{imdbId}";
|
||||||
|
playUrl = await InvokeCache<string>(cacheKey, TimeSpan.FromMinutes(5), async () =>
|
||||||
|
{
|
||||||
|
return await invoke.GetWormholePlay(imdbId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(playUrl))
|
||||||
|
{
|
||||||
|
return new ResolveResult
|
||||||
|
{
|
||||||
|
PlayUrl = playUrl,
|
||||||
|
IsSerial = IsSerialByUrl(playUrl, serial),
|
||||||
|
ShouldEnrich = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string searchQuery = originalTitle ?? title;
|
||||||
|
string searchCacheKey = $"makhno:uatut:search:{imdbId ?? searchQuery}";
|
||||||
|
|
||||||
|
var searchResults = await InvokeCache<List<SearchResult>>(searchCacheKey, TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
return await invoke.SearchUaTUT(searchQuery, imdbId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchResults == null || searchResults.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var selected = invoke.SelectUaTUTItem(searchResults, imdbId, year > 0 ? year : null, title, originalTitle);
|
||||||
|
if (selected == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var ashdiPath = await InvokeCache<string>($"makhno:ashdi:{selected.Id}", TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
return await invoke.GetAshdiPath(selected.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(ashdiPath))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
playUrl = invoke.BuildAshdiUrl(ashdiPath);
|
||||||
|
|
||||||
|
bool isSerial = serial == 1 || IsSerialByCategory(selected.Category, serial) || IsSerialByUrl(playUrl, serial);
|
||||||
|
|
||||||
|
return new ResolveResult
|
||||||
|
{
|
||||||
|
PlayUrl = playUrl,
|
||||||
|
AshdiPath = ashdiPath,
|
||||||
|
Selected = selected,
|
||||||
|
IsSerial = isSerial,
|
||||||
|
ShouldEnrich = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsSerialByCategory(string category, int serial)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(category))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (category.Equals("Аніме", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| category.Equals("Аниме", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return serial == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return category.Equals("Серіал", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| category.Equals("Сериал", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| category.Equals("Аніме", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| category.Equals("Аниме", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| category.Equals("Мультсеріал", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| category.Equals("Мультсериал", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| category.Equals("TV", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsSerialByUrl(string url, int serial)
|
||||||
|
{
|
||||||
|
if (serial == 1)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(url))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return url.Contains("/serial/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnrichWormhole(string imdbId, string title, string originalTitle, int year, ResolveResult resolved, MakhnoInvoke invoke)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(imdbId) || resolved?.Selected == null || string.IsNullOrWhiteSpace(resolved.AshdiPath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
int? yearValue = year > 0 ? year : null;
|
||||||
|
if (!yearValue.HasValue && int.TryParse(resolved.Selected.Year, out int parsedYear))
|
||||||
|
yearValue = parsedYear;
|
||||||
|
|
||||||
|
var tmdbResult = await invoke.FetchTmdbByImdb(imdbId, yearValue);
|
||||||
|
if (tmdbResult == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var (item, mediaType) = tmdbResult.Value;
|
||||||
|
var tmdbId = item.Value<long?>("id");
|
||||||
|
if (!tmdbId.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string original = item.Value<string>("original_title")
|
||||||
|
?? item.Value<string>("original_name")
|
||||||
|
?? resolved.Selected.TitleEn
|
||||||
|
?? originalTitle
|
||||||
|
?? title;
|
||||||
|
|
||||||
|
string resultTitle = resolved.Selected.Title
|
||||||
|
?? item.Value<string>("title")
|
||||||
|
?? item.Value<string>("name");
|
||||||
|
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
imdb_id = imdbId,
|
||||||
|
_id = $"{mediaType}:{tmdbId.Value}",
|
||||||
|
original_title = original,
|
||||||
|
title = resultTitle,
|
||||||
|
serial = mediaType == "tv" ? 1 : 0,
|
||||||
|
ashdi = resolved.AshdiPath,
|
||||||
|
year = (resolved.Selected.Year ?? yearValue?.ToString())
|
||||||
|
};
|
||||||
|
|
||||||
|
await invoke.PostWormholeAsync(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
||||||
|
{
|
||||||
|
string link = streamLink?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(link))
|
||||||
|
return link;
|
||||||
|
|
||||||
|
link = StripLampacArgs(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HostStreamProxy(init, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ResolveResult
|
||||||
|
{
|
||||||
|
public string PlayUrl { get; set; }
|
||||||
|
public string AshdiPath { get; set; }
|
||||||
|
public SearchResult Selected { get; set; }
|
||||||
|
public bool IsSerial { get; set; }
|
||||||
|
public bool ShouldEnrich { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Makhno/Makhno.csproj
Normal file
15
Makhno/Makhno.csproj
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<OutputType>library</OutputType>
|
||||||
|
<IsPackable>true</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Shared">
|
||||||
|
<HintPath>..\..\Shared.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
743
Makhno/MakhnoInvoke.cs
Normal file
743
Makhno/MakhnoInvoke.cs
Normal file
@ -0,0 +1,743 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Shared;
|
||||||
|
using Shared.Engine;
|
||||||
|
using Shared.Models;
|
||||||
|
using Shared.Models.Online.Settings;
|
||||||
|
using Makhno.Models;
|
||||||
|
|
||||||
|
namespace Makhno
|
||||||
|
{
|
||||||
|
public class MakhnoInvoke
|
||||||
|
{
|
||||||
|
private const string WormholeHost = "http://wormhole.lampame.v6.rocks/";
|
||||||
|
private const string AshdiHost = "https://ashdi.vip";
|
||||||
|
|
||||||
|
private readonly OnlinesSettings _init;
|
||||||
|
private readonly IHybridCache _hybridCache;
|
||||||
|
private readonly Action<string> _onLog;
|
||||||
|
private readonly ProxyManager _proxyManager;
|
||||||
|
|
||||||
|
public MakhnoInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||||
|
{
|
||||||
|
_init = init;
|
||||||
|
_hybridCache = hybridCache;
|
||||||
|
_onLog = onLog;
|
||||||
|
_proxyManager = proxyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetWormholePlay(string imdbId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(imdbId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string url = $"{WormholeHost}?imdb_id={imdbId}";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var headers = new List<HeadersModel>()
|
||||||
|
{
|
||||||
|
new HeadersModel("User-Agent", Http.UserAgent)
|
||||||
|
};
|
||||||
|
|
||||||
|
string response = await Http.Get(url, timeoutSeconds: 4, headers: headers, proxy: _proxyManager.Get());
|
||||||
|
if (string.IsNullOrWhiteSpace(response))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var payload = JsonConvert.DeserializeObject<WormholeResponse>(response);
|
||||||
|
return string.IsNullOrWhiteSpace(payload?.play) ? null : payload.play;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"Makhno wormhole error: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SearchResult>> SearchUaTUT(string query, string imdbId = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string searchUrl = $"{_init.apihost}/search.php";
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(imdbId))
|
||||||
|
{
|
||||||
|
var imdbResults = await PerformSearch(searchUrl, imdbId);
|
||||||
|
if (imdbResults?.Any() == true)
|
||||||
|
return imdbResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(query))
|
||||||
|
{
|
||||||
|
var titleResults = await PerformSearch(searchUrl, query);
|
||||||
|
return titleResults ?? new List<SearchResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<SearchResult>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"Makhno UaTUT search error: {ex.Message}");
|
||||||
|
return new List<SearchResult>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<SearchResult>> PerformSearch(string searchUrl, string query)
|
||||||
|
{
|
||||||
|
string url = $"{searchUrl}?q={WebUtility.UrlEncode(query)}";
|
||||||
|
_onLog($"Makhno UaTUT searching: {url}");
|
||||||
|
|
||||||
|
var headers = new List<HeadersModel>()
|
||||||
|
{
|
||||||
|
new HeadersModel("User-Agent", Http.UserAgent)
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await Http.Get(url, headers: headers, proxy: _proxyManager.Get());
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(response))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var results = JsonConvert.DeserializeObject<List<SearchResult>>(response);
|
||||||
|
_onLog($"Makhno UaTUT found {results?.Count ?? 0} results for query: {query}");
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"Makhno UaTUT parse error: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetMoviePageContent(string movieId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string url = $"{_init.apihost}/{movieId}";
|
||||||
|
_onLog($"Makhno UaTUT getting movie page: {url}");
|
||||||
|
|
||||||
|
var headers = new List<HeadersModel>()
|
||||||
|
{
|
||||||
|
new HeadersModel("User-Agent", Http.UserAgent)
|
||||||
|
};
|
||||||
|
var response = await Http.Get(url, headers: headers, proxy: _proxyManager.Get());
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"Makhno UaTUT GetMoviePageContent error: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetPlayerUrl(string moviePageContent)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(moviePageContent))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var match = Regex.Match(moviePageContent, @"<iframe[^>]*id=[""']vip-player[""'][^>]*src=[""']([^""']+)", RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success)
|
||||||
|
return NormalizePlayerUrl(match.Groups[1].Value);
|
||||||
|
|
||||||
|
match = Regex.Match(moviePageContent, @"<iframe[^>]*id=[""']alt-player[""'][^>]*src=[""']([^""']+)", RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success)
|
||||||
|
return NormalizePlayerUrl(match.Groups[1].Value);
|
||||||
|
|
||||||
|
var iframeMatches = Regex.Matches(moviePageContent, @"<iframe[^>]*(?:id=[""']([^""']+)[""'])?[^>]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase);
|
||||||
|
foreach (Match iframe in iframeMatches)
|
||||||
|
{
|
||||||
|
string iframeId = iframe.Groups[1].Value?.ToLowerInvariant();
|
||||||
|
string src = iframe.Groups[2].Value;
|
||||||
|
if (string.IsNullOrEmpty(src))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(iframeId) && iframeId.Contains("player"))
|
||||||
|
return NormalizePlayerUrl(src);
|
||||||
|
|
||||||
|
if (src.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
src.Contains("zetvideo.net", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
src.Contains("player", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return NormalizePlayerUrl(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
var urlMatch = Regex.Match(moviePageContent, @"(https?://[^""'\s>]+/(?:vod|serial)/\d+[^""'\s>]*)", RegexOptions.IgnoreCase);
|
||||||
|
if (urlMatch.Success)
|
||||||
|
return NormalizePlayerUrl(urlMatch.Groups[1].Value);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"Makhno UaTUT GetPlayerUrl error: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizePlayerUrl(string src)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(src))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (src.StartsWith("//"))
|
||||||
|
return $"https:{src}";
|
||||||
|
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PlayerData> GetPlayerData(string playerUrl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(playerUrl))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string requestUrl = playerUrl;
|
||||||
|
var headers = new List<HeadersModel>()
|
||||||
|
{
|
||||||
|
new HeadersModel("User-Agent", Http.UserAgent)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (playerUrl.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
headers.Add(new HeadersModel("Referer", "https://ashdi.vip/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ApnHelper.IsAshdiUrl(playerUrl) && ApnHelper.IsEnabled(_init))
|
||||||
|
requestUrl = ApnHelper.WrapUrl(_init, playerUrl);
|
||||||
|
|
||||||
|
_onLog($"Makhno getting player data from: {requestUrl}");
|
||||||
|
|
||||||
|
var response = await Http.Get(requestUrl, headers: headers, proxy: _proxyManager.Get());
|
||||||
|
if (string.IsNullOrEmpty(response))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return ParsePlayerData(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"Makhno GetPlayerData error: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayerData ParsePlayerData(string html)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(html))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var fileMatch = Regex.Match(html, @"file:'([^']+)'", RegexOptions.IgnoreCase);
|
||||||
|
if (!fileMatch.Success)
|
||||||
|
fileMatch = Regex.Match(html, @"file:\s*""([^""]+)""", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("["))
|
||||||
|
{
|
||||||
|
var posterMatch = Regex.Match(html, @"poster:[""']([^""']+)[""']", RegexOptions.IgnoreCase);
|
||||||
|
return new PlayerData
|
||||||
|
{
|
||||||
|
File = fileMatch.Groups[1].Value,
|
||||||
|
Poster = posterMatch.Success ? posterMatch.Groups[1].Value : null,
|
||||||
|
Voices = new List<Voice>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
string jsonData = ExtractPlayerJson(html);
|
||||||
|
if (jsonData == null)
|
||||||
|
_onLog("Makhno ParsePlayerData: file array not found");
|
||||||
|
else
|
||||||
|
_onLog($"Makhno ParsePlayerData: file array length={jsonData.Length}");
|
||||||
|
if (!string.IsNullOrEmpty(jsonData))
|
||||||
|
{
|
||||||
|
var voices = ParseVoicesJson(jsonData);
|
||||||
|
_onLog($"Makhno ParsePlayerData: voices={voices?.Count ?? 0}");
|
||||||
|
return new PlayerData
|
||||||
|
{
|
||||||
|
File = null,
|
||||||
|
Poster = null,
|
||||||
|
Voices = voices
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var m3u8Match = Regex.Match(html, @"(https?://[^""'\s>]+\.m3u8[^""'\s>]*)", RegexOptions.IgnoreCase);
|
||||||
|
if (m3u8Match.Success)
|
||||||
|
{
|
||||||
|
_onLog("Makhno ParsePlayerData: fallback m3u8 match");
|
||||||
|
return new PlayerData
|
||||||
|
{
|
||||||
|
File = m3u8Match.Groups[1].Value,
|
||||||
|
Poster = null,
|
||||||
|
Voices = new List<Voice>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceMatch = Regex.Match(html, @"<source[^>]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase);
|
||||||
|
if (sourceMatch.Success)
|
||||||
|
{
|
||||||
|
_onLog("Makhno ParsePlayerData: fallback source match");
|
||||||
|
return new PlayerData
|
||||||
|
{
|
||||||
|
File = sourceMatch.Groups[1].Value,
|
||||||
|
Poster = null,
|
||||||
|
Voices = new List<Voice>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"Makhno ParsePlayerData error: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Voice> ParseVoicesJson(string jsonData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var voicesArray = JsonConvert.DeserializeObject<List<JObject>>(jsonData);
|
||||||
|
var voices = new List<Voice>();
|
||||||
|
|
||||||
|
if (voicesArray == null)
|
||||||
|
return voices;
|
||||||
|
|
||||||
|
foreach (var voiceGroup in voicesArray)
|
||||||
|
{
|
||||||
|
var voice = new Voice
|
||||||
|
{
|
||||||
|
Name = voiceGroup["title"]?.ToString(),
|
||||||
|
Seasons = new List<Season>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var seasons = voiceGroup["folder"] as JArray;
|
||||||
|
if (seasons != null)
|
||||||
|
{
|
||||||
|
foreach (var seasonGroup in seasons)
|
||||||
|
{
|
||||||
|
string seasonTitle = seasonGroup["title"]?.ToString() ?? string.Empty;
|
||||||
|
var episodes = new List<Episode>();
|
||||||
|
|
||||||
|
var episodesArray = seasonGroup["folder"] as JArray;
|
||||||
|
if (episodesArray != null)
|
||||||
|
{
|
||||||
|
foreach (var episode in episodesArray)
|
||||||
|
{
|
||||||
|
episodes.Add(new Episode
|
||||||
|
{
|
||||||
|
Id = episode["id"]?.ToString(),
|
||||||
|
Title = episode["title"]?.ToString(),
|
||||||
|
File = episode["file"]?.ToString(),
|
||||||
|
Poster = episode["poster"]?.ToString(),
|
||||||
|
Subtitle = episode["subtitle"]?.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
episodes = episodes
|
||||||
|
.OrderBy(item => ExtractEpisodeNumber(item.Title) is null)
|
||||||
|
.ThenBy(item => ExtractEpisodeNumber(item.Title) ?? 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
voice.Seasons.Add(new Season
|
||||||
|
{
|
||||||
|
Title = seasonTitle,
|
||||||
|
Episodes = episodes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
voices.Add(voice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return voices;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"Makhno ParseVoicesJson error: {ex.Message}");
|
||||||
|
return new List<Voice>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractPlayerJson(string html)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(html))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var startIndex = FindFileArrayStart(html);
|
||||||
|
if (startIndex < 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string jsonArray = ExtractBracketArray(html, startIndex);
|
||||||
|
if (string.IsNullOrEmpty(jsonArray))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return jsonArray
|
||||||
|
.Replace("\\'", "'")
|
||||||
|
.Replace("\\\"", "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int FindFileArrayStart(string html)
|
||||||
|
{
|
||||||
|
int playerStart = html.IndexOf("Playerjs", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (playerStart >= 0)
|
||||||
|
{
|
||||||
|
int playerIndex = FindFileArrayStartInRange(html, playerStart);
|
||||||
|
if (playerIndex >= 0)
|
||||||
|
return playerIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = FindFileArrayIndex(html, "file:'[");
|
||||||
|
if (index >= 0)
|
||||||
|
return index;
|
||||||
|
|
||||||
|
index = FindFileArrayIndex(html, "file:\"[");
|
||||||
|
if (index >= 0)
|
||||||
|
return index;
|
||||||
|
|
||||||
|
var match = Regex.Match(html, @"file\s*:\s*'?\[", RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success)
|
||||||
|
return match.Index + match.Value.LastIndexOf('[');
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int FindFileArrayStartInRange(string html, int startIndex)
|
||||||
|
{
|
||||||
|
int searchStart = startIndex;
|
||||||
|
int searchEnd = Math.Min(html.Length, startIndex + 200000);
|
||||||
|
|
||||||
|
int tokenIndex = IndexOfIgnoreCase(html, "file:'[", searchStart, searchEnd);
|
||||||
|
if (tokenIndex >= 0)
|
||||||
|
return html.IndexOf('[', tokenIndex);
|
||||||
|
|
||||||
|
tokenIndex = IndexOfIgnoreCase(html, "file:\"[", searchStart, searchEnd);
|
||||||
|
if (tokenIndex >= 0)
|
||||||
|
return html.IndexOf('[', tokenIndex);
|
||||||
|
|
||||||
|
tokenIndex = IndexOfIgnoreCase(html, "file", searchStart, searchEnd);
|
||||||
|
if (tokenIndex >= 0)
|
||||||
|
{
|
||||||
|
int bracketIndex = html.IndexOf('[', tokenIndex);
|
||||||
|
if (bracketIndex >= 0 && bracketIndex < searchEnd)
|
||||||
|
return bracketIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int IndexOfIgnoreCase(string text, string value, int startIndex, int endIndex)
|
||||||
|
{
|
||||||
|
int index = text.IndexOf(value, startIndex, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (index >= 0 && index < endIndex)
|
||||||
|
return index;
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int FindFileArrayIndex(string html, string token)
|
||||||
|
{
|
||||||
|
int tokenIndex = html.IndexOf(token, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (tokenIndex < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
int bracketIndex = html.IndexOf('[', tokenIndex);
|
||||||
|
return bracketIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractBracketArray(string text, int startIndex)
|
||||||
|
{
|
||||||
|
if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[')
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int depth = 0;
|
||||||
|
bool inString = false;
|
||||||
|
bool escape = false;
|
||||||
|
char quoteChar = '\0';
|
||||||
|
|
||||||
|
for (int i = startIndex; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
char ch = text[i];
|
||||||
|
|
||||||
|
if (inString)
|
||||||
|
{
|
||||||
|
if (escape)
|
||||||
|
{
|
||||||
|
escape = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '\\')
|
||||||
|
{
|
||||||
|
escape = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == quoteChar)
|
||||||
|
{
|
||||||
|
inString = false;
|
||||||
|
quoteChar = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '"' || ch == '\'')
|
||||||
|
{
|
||||||
|
inString = true;
|
||||||
|
quoteChar = ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '[')
|
||||||
|
{
|
||||||
|
depth++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == ']')
|
||||||
|
{
|
||||||
|
depth--;
|
||||||
|
if (depth == 0)
|
||||||
|
return text.Substring(startIndex, i - startIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int? ExtractEpisodeNumber(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var match = Regex.Match(value, @"(\d+)");
|
||||||
|
if (!match.Success)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (int.TryParse(match.Groups[1].Value, out int num))
|
||||||
|
return num;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ExtractAshdiPath(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var match = Regex.Match(value, @"https?://(?:www\.)?ashdi\.vip/((?:vod|serial)/\d+)", RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success)
|
||||||
|
return match.Groups[1].Value;
|
||||||
|
|
||||||
|
match = Regex.Match(value, @"\b((?:vod|serial)/\d+)\b", RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success)
|
||||||
|
return match.Groups[1].Value;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BuildAshdiUrl(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return path;
|
||||||
|
|
||||||
|
return $"{AshdiHost}/{path.TrimStart('/')}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetAshdiPath(string movieId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(movieId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var page = await GetMoviePageContent(movieId);
|
||||||
|
if (string.IsNullOrWhiteSpace(page))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var playerUrl = GetPlayerUrl(page);
|
||||||
|
var path = ExtractAshdiPath(playerUrl);
|
||||||
|
if (!string.IsNullOrWhiteSpace(path))
|
||||||
|
return path;
|
||||||
|
|
||||||
|
return ExtractAshdiPath(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchResult SelectUaTUTItem(List<SearchResult> items, string imdbId, int? year, string title, string titleEn)
|
||||||
|
{
|
||||||
|
if (items == null || items.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var candidates = items.Where(item => ImdbMatch(item, imdbId) && YearMatch(item, year)).ToList();
|
||||||
|
if (candidates.Count == 1)
|
||||||
|
return candidates[0];
|
||||||
|
if (candidates.Count > 1)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
candidates = items.Where(item => ImdbMatch(item, imdbId) && TitleMatch(item, title, titleEn)).ToList();
|
||||||
|
if (candidates.Count == 1)
|
||||||
|
return candidates[0];
|
||||||
|
if (candidates.Count > 1)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
candidates = items.Where(item => YearMatch(item, year) && TitleMatch(item, title, titleEn)).ToList();
|
||||||
|
if (candidates.Count == 1)
|
||||||
|
return candidates[0];
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ImdbMatch(SearchResult item, string imdbId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(imdbId) || item == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return string.Equals(item.ImdbId?.Trim(), imdbId.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool YearMatch(SearchResult item, int? year)
|
||||||
|
{
|
||||||
|
if (year == null || item == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var itemYear = YearInt(item.Year);
|
||||||
|
return itemYear.HasValue && itemYear.Value == year.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TitleMatch(SearchResult item, string title, string titleEn)
|
||||||
|
{
|
||||||
|
if (item == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
string itemTitle = NormalizeTitle(item.Title);
|
||||||
|
string itemTitleEn = NormalizeTitle(item.TitleEn);
|
||||||
|
string targetTitle = NormalizeTitle(title);
|
||||||
|
string targetTitleEn = NormalizeTitle(titleEn);
|
||||||
|
|
||||||
|
return (itemTitle.Length > 0 && targetTitle.Length > 0 && itemTitle == targetTitle)
|
||||||
|
|| (itemTitle.Length > 0 && targetTitleEn.Length > 0 && itemTitle == targetTitleEn)
|
||||||
|
|| (itemTitleEn.Length > 0 && targetTitle.Length > 0 && itemTitleEn == targetTitle)
|
||||||
|
|| (itemTitleEn.Length > 0 && targetTitleEn.Length > 0 && itemTitleEn == targetTitleEn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeTitle(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
string text = value.ToLowerInvariant();
|
||||||
|
text = Regex.Replace(text, @"[^\w\s]+", " ");
|
||||||
|
text = Regex.Replace(text, @"\b(season|сезон|частина|part|ova|special|movie|film)\b", " ");
|
||||||
|
text = Regex.Replace(text, @"\b(\d+)(st|nd|rd|th)\b", "$1");
|
||||||
|
text = Regex.Replace(text, @"\b\d+\b", " ");
|
||||||
|
text = Regex.Replace(text, @"\s+", " ");
|
||||||
|
return text.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int? YearInt(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (int.TryParse(value.Trim(), out int result))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(JObject item, string mediaType)?> FetchTmdbByImdb(string imdbId, int? year)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(imdbId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string apiKey = AppInit.conf?.tmdb?.api_key;
|
||||||
|
if (string.IsNullOrWhiteSpace(apiKey))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string tmdbUrl = $"http://{AppInit.conf.listen.localhost}:{AppInit.conf.listen.port}/tmdb/api/3/find/{imdbId}?external_source=imdb_id&api_key={apiKey}&language=en-US";
|
||||||
|
|
||||||
|
var headers = new List<HeadersModel>()
|
||||||
|
{
|
||||||
|
new HeadersModel("User-Agent", Http.UserAgent)
|
||||||
|
};
|
||||||
|
|
||||||
|
JObject payload = await Http.Get<JObject>(tmdbUrl, timeoutSeconds: 6, headers: headers);
|
||||||
|
if (payload == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var movieResults = payload["movie_results"] as JArray ?? new JArray();
|
||||||
|
var tvResults = payload["tv_results"] as JArray ?? new JArray();
|
||||||
|
|
||||||
|
var candidates = new List<(JObject item, string mediaType)>();
|
||||||
|
foreach (var item in movieResults.OfType<JObject>())
|
||||||
|
candidates.Add((item, "movie"));
|
||||||
|
foreach (var item in tvResults.OfType<JObject>())
|
||||||
|
candidates.Add((item, "tv"));
|
||||||
|
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (year.HasValue)
|
||||||
|
{
|
||||||
|
string yearText = year.Value.ToString();
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
string dateValue = candidate.mediaType == "movie"
|
||||||
|
? candidate.item.Value<string>("release_date")
|
||||||
|
: candidate.item.Value<string>("first_air_date");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(dateValue) && dateValue.StartsWith(yearText, StringComparison.Ordinal))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates[0];
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"Makhno TMDB fetch failed: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> PostWormholeAsync(object payload)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var headers = new List<HeadersModel>()
|
||||||
|
{
|
||||||
|
new HeadersModel("Content-Type", "application/json"),
|
||||||
|
new HeadersModel("User-Agent", Http.UserAgent)
|
||||||
|
};
|
||||||
|
|
||||||
|
string json = JsonConvert.SerializeObject(payload, Formatting.None);
|
||||||
|
await Http.Post(WormholeHost, json, timeoutSeconds: 6, headers: headers, proxy: _proxyManager.Get());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"Makhno wormhole insert failed: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class WormholeResponse
|
||||||
|
{
|
||||||
|
public string play { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
199
Makhno/ModInit.cs
Normal file
199
Makhno/ModInit.cs
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Shared;
|
||||||
|
using Shared.Engine;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Shared.Models.Online.Settings;
|
||||||
|
using Shared.Models.Module;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.CodeAnalysis.Scripting;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Shared.Models;
|
||||||
|
using Shared.Models.Events;
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Mime;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Authentication;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Makhno
|
||||||
|
{
|
||||||
|
public class ModInit
|
||||||
|
{
|
||||||
|
public static double Version => 1.7;
|
||||||
|
|
||||||
|
public static OnlinesSettings Makhno;
|
||||||
|
public static bool ApnHostProvided;
|
||||||
|
|
||||||
|
public static OnlinesSettings Settings
|
||||||
|
{
|
||||||
|
get => Makhno;
|
||||||
|
set => Makhno = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// модуль загружен
|
||||||
|
/// </summary>
|
||||||
|
public static void loaded(InitspaceModel initspace)
|
||||||
|
{
|
||||||
|
Makhno = new OnlinesSettings("Makhno", "https://wormhole.lampame.v6.rocks", streamproxy: false, useproxy: false)
|
||||||
|
{
|
||||||
|
displayname = "Махно",
|
||||||
|
displayindex = 0,
|
||||||
|
apihost = "https://uk.uatut.fun/watch",
|
||||||
|
proxy = new Shared.Models.Base.ProxySettings()
|
||||||
|
{
|
||||||
|
useAuth = true,
|
||||||
|
username = "",
|
||||||
|
password = "",
|
||||||
|
list = new string[] { "socks5://ip:port" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var conf = ModuleInvoke.Conf("Makhno", Makhno);
|
||||||
|
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||||
|
if (hasApn)
|
||||||
|
{
|
||||||
|
conf.Remove("apn");
|
||||||
|
conf.Remove("apn_host");
|
||||||
|
}
|
||||||
|
Makhno = conf.ToObject<OnlinesSettings>();
|
||||||
|
if (hasApn)
|
||||||
|
ApnHelper.ApplyInitConf(apnEnabled, apnHost, Makhno);
|
||||||
|
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
|
||||||
|
if (hasApn && apnEnabled)
|
||||||
|
{
|
||||||
|
Makhno.streamproxy = false;
|
||||||
|
}
|
||||||
|
else if (Makhno.streamproxy)
|
||||||
|
{
|
||||||
|
Makhno.apnstream = false;
|
||||||
|
Makhno.apn = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Виводити "уточнити пошук"
|
||||||
|
AppInit.conf.online.with_search.Add("makhno");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UpdateService
|
||||||
|
{
|
||||||
|
private static readonly string _connectUrl = "https://lmcuk.lampame.v6.rocks/stats";
|
||||||
|
|
||||||
|
private static ConnectResponse? Connect = null;
|
||||||
|
private static DateTime? _connectTime = null;
|
||||||
|
private static DateTime? _disconnectTime = null;
|
||||||
|
|
||||||
|
private static readonly TimeSpan _resetInterval = TimeSpan.FromHours(4);
|
||||||
|
private static Timer? _resetTimer = null;
|
||||||
|
|
||||||
|
private static readonly object _lock = new();
|
||||||
|
|
||||||
|
public static async Task ConnectAsync(string host, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (_connectTime is not null || Connect?.IsUpdateUnavailable == true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_connectTime is not null || Connect?.IsUpdateUnavailable == true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var handler = new SocketsHttpHandler
|
||||||
|
{
|
||||||
|
SslOptions = new SslClientAuthenticationOptions
|
||||||
|
{
|
||||||
|
RemoteCertificateValidationCallback = (_, _, _, _) => true,
|
||||||
|
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using var client = new HttpClient(handler);
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(15);
|
||||||
|
|
||||||
|
var request = new
|
||||||
|
{
|
||||||
|
Host = host,
|
||||||
|
Module = ModInit.Settings.plugin,
|
||||||
|
Version = ModInit.Version,
|
||||||
|
};
|
||||||
|
|
||||||
|
var requestJson = JsonConvert.SerializeObject(request, Formatting.None);
|
||||||
|
var requestContent = new StringContent(requestJson, Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||||
|
|
||||||
|
var response = await client
|
||||||
|
.PostAsync(_connectUrl, requestContent, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
if (response.Content.Headers.ContentLength > 0)
|
||||||
|
{
|
||||||
|
var responseText = await response.Content
|
||||||
|
.ReadAsStringAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
Connect = JsonConvert.DeserializeObject<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, 16))
|
||||||
|
: DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
ResetConnectTime(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ResetConnectTime(object? state)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_connectTime = null;
|
||||||
|
Connect = null;
|
||||||
|
|
||||||
|
_resetTimer?.Dispose();
|
||||||
|
_resetTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static bool IsDisconnected()
|
||||||
|
{
|
||||||
|
return _disconnectTime is not null
|
||||||
|
&& DateTime.UtcNow >= _disconnectTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ActionResult Validate(ActionResult result)
|
||||||
|
{
|
||||||
|
return IsDisconnected()
|
||||||
|
? throw new JsonReaderException($"Disconnect error: {Guid.CreateVersion7()}")
|
||||||
|
: result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ConnectResponse(bool IsUpdateUnavailable, bool IsNoiseEnabled);
|
||||||
|
}
|
||||||
61
Makhno/Models/MakhnoModels.cs
Normal file
61
Makhno/Models/MakhnoModels.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Makhno.Models
|
||||||
|
{
|
||||||
|
public class SearchResult
|
||||||
|
{
|
||||||
|
[JsonProperty("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("imdb_id")]
|
||||||
|
public string ImdbId { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("title")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("title_alt")]
|
||||||
|
public string TitleAlt { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("title_en")]
|
||||||
|
public string TitleEn { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("title_ru")]
|
||||||
|
public string TitleRu { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("year")]
|
||||||
|
public string Year { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("category")]
|
||||||
|
public string Category { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerData
|
||||||
|
{
|
||||||
|
public string File { get; set; }
|
||||||
|
public string Poster { get; set; }
|
||||||
|
public List<Voice> Voices { get; set; }
|
||||||
|
public List<Season> Seasons { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Voice
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public List<Season> Seasons { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Season
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public List<Episode> Episodes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Episode
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string File { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Poster { get; set; }
|
||||||
|
public string Subtitle { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Makhno/OnlineApi.cs
Normal file
40
Makhno/OnlineApi.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Shared.Models;
|
||||||
|
using Shared.Models.Base;
|
||||||
|
using Shared.Models.Module;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Makhno
|
||||||
|
{
|
||||||
|
public class OnlineApi
|
||||||
|
{
|
||||||
|
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||||
|
HttpContext httpContext,
|
||||||
|
IMemoryCache memoryCache,
|
||||||
|
RequestModel requestInfo,
|
||||||
|
string host,
|
||||||
|
OnlineEventsModel args)
|
||||||
|
{
|
||||||
|
long.TryParse(args.id, out long tmdbid);
|
||||||
|
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
|
||||||
|
{
|
||||||
|
var online = new List<(string name, string url, string plugin, int index)>();
|
||||||
|
|
||||||
|
var init = ModInit.Makhno;
|
||||||
|
if (init.enable && !init.rip)
|
||||||
|
{
|
||||||
|
string url = init.overridehost;
|
||||||
|
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||||
|
url = $"{host}/makhno";
|
||||||
|
|
||||||
|
online.Add((init.displayname, url, "makhno", init.displayindex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return online;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Makhno/manifest.json
Normal file
6
Makhno/manifest.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"enable": true,
|
||||||
|
"version": 3,
|
||||||
|
"initspace": "Makhno.ModInit",
|
||||||
|
"online": "Makhno.OnlineApi"
|
||||||
|
}
|
||||||
@ -24,7 +24,7 @@ namespace Mikai.Controllers
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("mikai")]
|
[Route("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)
|
public async Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, bool rjson = false, bool checksearch = false)
|
||||||
{
|
{
|
||||||
await UpdateService.ConnectAsync(host);
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
@ -33,6 +33,19 @@ namespace Mikai.Controllers
|
|||||||
return Forbid();
|
return Forbid();
|
||||||
|
|
||||||
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager);
|
var invoke = new MikaiInvoke(init, hybridCache, OnLog, _proxyManager);
|
||||||
|
|
||||||
|
if (checksearch)
|
||||||
|
{
|
||||||
|
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||||
|
return OnError("mikai", _proxyManager);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
OnLog($"Mikai Index: title={title}, original_title={original_title}, serial={serial}, s={s}, t={t}, year={year}");
|
OnLog($"Mikai Index: title={title}, original_title={original_title}, serial={serial}, s={s}, t={t}, year={year}");
|
||||||
|
|
||||||
var searchResults = await invoke.Search(title, original_title, year);
|
var searchResults = await invoke.Search(title, original_title, year);
|
||||||
@ -57,11 +70,15 @@ namespace Mikai.Controllers
|
|||||||
|
|
||||||
if (isSerial)
|
if (isSerial)
|
||||||
{
|
{
|
||||||
var seasonNumbers = voices.Values
|
MikaiVoiceInfo voiceForSeasons = null;
|
||||||
.SelectMany(v => v.Seasons.Keys)
|
bool restrictByVoice = !string.IsNullOrEmpty(t) && voices.TryGetValue(t, out voiceForSeasons);
|
||||||
.Distinct()
|
var seasonNumbers = restrictByVoice
|
||||||
.OrderBy(n => n)
|
? GetSeasonSet(voiceForSeasons).OrderBy(n => n).ToList()
|
||||||
.ToList();
|
: voices.Values
|
||||||
|
.SelectMany(v => GetSeasonSet(v))
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(n => n)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
if (seasonNumbers.Count == 0)
|
if (seasonNumbers.Count == 0)
|
||||||
return OnError("mikai", _proxyManager);
|
return OnError("mikai", _proxyManager);
|
||||||
@ -72,6 +89,8 @@ namespace Mikai.Controllers
|
|||||||
foreach (var seasonNumber in seasonNumbers)
|
foreach (var seasonNumber in seasonNumbers)
|
||||||
{
|
{
|
||||||
string link = $"{host}/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}";
|
string link = $"{host}/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());
|
seasonTpl.Append($"{seasonNumber}", link, seasonNumber.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,16 +108,29 @@ namespace Mikai.Controllers
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(t))
|
if (string.IsNullOrEmpty(t))
|
||||||
t = voicesForSeason[0].Key;
|
t = voicesForSeason[0].Key;
|
||||||
|
else if (!voices.ContainsKey(t))
|
||||||
|
t = voicesForSeason[0].Key;
|
||||||
|
|
||||||
var voiceTpl = new VoiceTpl();
|
var voiceTpl = new VoiceTpl();
|
||||||
|
var selectedVoiceInfo = voices[t];
|
||||||
|
var selectedSeasonSet = GetSeasonSet(selectedVoiceInfo);
|
||||||
foreach (var voice in voicesForSeason)
|
foreach (var voice in voicesForSeason)
|
||||||
{
|
{
|
||||||
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&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
|
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";
|
||||||
|
if (sameSeasonSet)
|
||||||
|
voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
|
||||||
|
else
|
||||||
|
voiceLink += $"&s=-1&t={HttpUtility.UrlEncode(voice.Key)}";
|
||||||
voiceTpl.Append(voice.Key, voice.Key == t, voiceLink);
|
voiceTpl.Append(voice.Key, voice.Key == t, voiceLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!voices.ContainsKey(t) || !voices[t].Seasons.ContainsKey(s))
|
if (!voices.ContainsKey(t) || !voices[t].Seasons.ContainsKey(s))
|
||||||
return OnError("mikai", _proxyManager);
|
{
|
||||||
|
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)}";
|
||||||
|
return Redirect(redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
var episodeTpl = new EpisodeTpl();
|
var episodeTpl = new EpisodeTpl();
|
||||||
foreach (var ep in voices[t].Seasons[s].OrderBy(e => e.Number))
|
foreach (var ep in voices[t].Seasons[s].OrderBy(e => e.Number))
|
||||||
@ -116,7 +148,7 @@ namespace Mikai.Controllers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string playUrl = HostStreamProxy(init, accsArgs(streamLink));
|
string playUrl = BuildStreamUrl(init, streamLink, headers: null, forceProxy: false);
|
||||||
episodeTpl.Append(episodeName, displayTitle, s.ToString(), ep.Number.ToString(), playUrl);
|
episodeTpl.Append(episodeName, displayTitle, s.ToString(), ep.Number.ToString(), playUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,7 +174,7 @@ namespace Mikai.Controllers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string playUrl = HostStreamProxy(init, accsArgs(episode.Url));
|
string playUrl = BuildStreamUrl(init, episode.Url, headers: null, forceProxy: false);
|
||||||
movieTpl.Append(voice.DisplayName, playUrl);
|
movieTpl.Append(voice.DisplayName, playUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -367,9 +399,41 @@ namespace Mikai.Controllers
|
|||||||
streamLink.Contains("moonanime.art", StringComparison.OrdinalIgnoreCase);
|
streamLink.Contains("moonanime.art", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static HashSet<int> GetSeasonSet(MikaiVoiceInfo voice)
|
||||||
|
{
|
||||||
|
if (voice?.Seasons == null || voice.Seasons.Count == 0)
|
||||||
|
return new HashSet<int>();
|
||||||
|
|
||||||
|
return voice.Seasons
|
||||||
|
.Where(kv => kv.Value != null && kv.Value.Any(ep => !string.IsNullOrEmpty(ep.Url)))
|
||||||
|
.Select(kv => kv.Key)
|
||||||
|
.ToHashSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 string BuildStreamUrl(OnlinesSettings init, string streamLink, List<HeadersModel> headers, bool forceProxy)
|
private string BuildStreamUrl(OnlinesSettings init, string streamLink, List<HeadersModel> headers, bool forceProxy)
|
||||||
{
|
{
|
||||||
string link = accsArgs(streamLink);
|
string link = streamLink?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(link))
|
||||||
|
return link;
|
||||||
|
|
||||||
|
link = StripLampacArgs(link);
|
||||||
|
|
||||||
if (ApnHelper.IsEnabled(init))
|
if (ApnHelper.IsEnabled(init))
|
||||||
{
|
{
|
||||||
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
||||||
|
|||||||
@ -24,7 +24,7 @@ namespace Mikai
|
|||||||
{
|
{
|
||||||
public class ModInit
|
public class ModInit
|
||||||
{
|
{
|
||||||
public static double Version => 3.3;
|
public static double Version => 3.5;
|
||||||
|
|
||||||
public static OnlinesSettings Mikai;
|
public static OnlinesSettings Mikai;
|
||||||
public static bool ApnHostProvided;
|
public static bool ApnHostProvided;
|
||||||
|
|||||||
@ -25,7 +25,7 @@ namespace StarLight.Controllers
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("starlight")]
|
[Route("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)
|
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, int s = -1, bool rjson = false, string href = null, bool checksearch = false)
|
||||||
{
|
{
|
||||||
await UpdateService.ConnectAsync(host);
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
@ -35,6 +35,18 @@ namespace StarLight.Controllers
|
|||||||
|
|
||||||
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager);
|
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
|
||||||
|
if (checksearch)
|
||||||
|
{
|
||||||
|
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||||
|
return OnError("starlight", proxyManager);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
string itemUrl = href;
|
string itemUrl = href;
|
||||||
if (string.IsNullOrEmpty(itemUrl))
|
if (string.IsNullOrEmpty(itemUrl))
|
||||||
{
|
{
|
||||||
@ -169,7 +181,10 @@ namespace StarLight.Controllers
|
|||||||
|
|
||||||
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
||||||
{
|
{
|
||||||
string link = accsArgs(streamLink);
|
string link = StripLampacArgs(streamLink?.Trim());
|
||||||
|
if (string.IsNullOrEmpty(link))
|
||||||
|
return link;
|
||||||
|
|
||||||
if (ApnHelper.IsEnabled(init))
|
if (ApnHelper.IsEnabled(init))
|
||||||
{
|
{
|
||||||
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
||||||
@ -184,6 +199,22 @@ namespace StarLight.Controllers
|
|||||||
return HostStreamProxy(init, link, proxy: proxyManager.Get());
|
return HostStreamProxy(init, link, proxy: proxyManager.Get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 string GetSeasonNumber(SeasonInfo season, int fallbackIndex)
|
private static string GetSeasonNumber(SeasonInfo season, int fallbackIndex)
|
||||||
{
|
{
|
||||||
if (season?.Title == null)
|
if (season?.Title == null)
|
||||||
|
|||||||
@ -23,7 +23,7 @@ namespace UAKino.Controllers
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("uakino")]
|
[Route("uakino")]
|
||||||
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, bool rjson = false, string href = null)
|
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, bool rjson = false, string href = null, bool checksearch = false)
|
||||||
{
|
{
|
||||||
await UpdateService.ConnectAsync(host);
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
@ -33,6 +33,18 @@ namespace UAKino.Controllers
|
|||||||
|
|
||||||
var invoke = new UAKinoInvoke(init, hybridCache, OnLog, proxyManager);
|
var invoke = new UAKinoInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
|
||||||
|
if (checksearch)
|
||||||
|
{
|
||||||
|
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||||
|
return OnError("uakino", proxyManager);
|
||||||
|
|
||||||
|
var searchResults = await invoke.Search(title, original_title, serial);
|
||||||
|
if (searchResults != null && searchResults.Count > 0)
|
||||||
|
return Content("data-json=", "text/plain; charset=utf-8");
|
||||||
|
|
||||||
|
return OnError("uakino", proxyManager);
|
||||||
|
}
|
||||||
|
|
||||||
string itemUrl = href;
|
string itemUrl = href;
|
||||||
if (string.IsNullOrEmpty(itemUrl))
|
if (string.IsNullOrEmpty(itemUrl))
|
||||||
{
|
{
|
||||||
@ -90,7 +102,29 @@ namespace UAKino.Controllers
|
|||||||
int episodeNumber = UAKinoInvoke.TryParseEpisodeNumber(ep.Title) ?? index;
|
int episodeNumber = UAKinoInvoke.TryParseEpisodeNumber(ep.Title) ?? index;
|
||||||
string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {episodeNumber}" : ep.Title;
|
string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {episodeNumber}" : ep.Title;
|
||||||
string callUrl = $"{host}/uakino/play?url={HttpUtility.UrlEncode(ep.Url)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
|
string callUrl = $"{host}/uakino/play?url={HttpUtility.UrlEncode(ep.Url)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
|
||||||
episode_tpl.Append(episodeName, title ?? original_title, "1", episodeNumber.ToString("D2"), accsArgs(callUrl), "call");
|
if (!string.IsNullOrEmpty(ep.Url) && ep.Url.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
string playUrl = BuildStreamUrl(init, ep.Url);
|
||||||
|
episode_tpl.Append(
|
||||||
|
episodeName,
|
||||||
|
title ?? original_title,
|
||||||
|
"1",
|
||||||
|
episodeNumber.ToString("D2"),
|
||||||
|
playUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
episode_tpl.Append(
|
||||||
|
episodeName,
|
||||||
|
title ?? original_title,
|
||||||
|
"1",
|
||||||
|
episodeNumber.ToString("D2"),
|
||||||
|
accsArgs(callUrl),
|
||||||
|
"call",
|
||||||
|
streamlink: accsArgs($"{callUrl}&play=true")
|
||||||
|
);
|
||||||
|
}
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,9 +177,30 @@ namespace UAKino.Controllers
|
|||||||
return UpdateService.Validate(Content(jsonResult, "application/json; charset=utf-8"));
|
return UpdateService.Validate(Content(jsonResult, "application/json; charset=utf-8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
||||||
{
|
{
|
||||||
string link = accsArgs(streamLink);
|
string link = streamLink?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(link))
|
||||||
|
return link;
|
||||||
|
|
||||||
|
link = StripLampacArgs(link);
|
||||||
|
|
||||||
if (ApnHelper.IsEnabled(init))
|
if (ApnHelper.IsEnabled(init))
|
||||||
{
|
{
|
||||||
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
||||||
|
|||||||
@ -23,7 +23,7 @@ namespace UAKino
|
|||||||
{
|
{
|
||||||
public class ModInit
|
public class ModInit
|
||||||
{
|
{
|
||||||
public static double Version => 3.3;
|
public static double Version => 010100100100100101010000;
|
||||||
|
|
||||||
public static OnlinesSettings UAKino;
|
public static OnlinesSettings UAKino;
|
||||||
public static bool ApnHostProvided;
|
public static bool ApnHostProvided;
|
||||||
|
|||||||
@ -25,7 +25,7 @@ namespace UaTUT
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
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 season = -1, bool rjson = false)
|
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, int season = -1, bool rjson = false, bool checksearch = false)
|
||||||
{
|
{
|
||||||
await UpdateService.ConnectAsync(host);
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
@ -45,6 +45,17 @@ namespace UaTUT
|
|||||||
return await invoke.Search(original_title ?? title, imdb_id);
|
return await invoke.Search(original_title ?? title, imdb_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (checksearch)
|
||||||
|
{
|
||||||
|
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
if (searchResults != null && searchResults.Any())
|
||||||
|
return Content("data-json=", "text/plain; charset=utf-8");
|
||||||
|
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
if (searchResults == null || !searchResults.Any())
|
if (searchResults == null || !searchResults.Any())
|
||||||
{
|
{
|
||||||
OnLog("UaTUT: No search results found");
|
OnLog("UaTUT: No search results found");
|
||||||
@ -53,20 +64,20 @@ namespace UaTUT
|
|||||||
|
|
||||||
if (serial == 1)
|
if (serial == 1)
|
||||||
{
|
{
|
||||||
return await HandleSeries(searchResults, imdb_id, kinopoisk_id, title, original_title, year, s, season, t, rjson, invoke);
|
return await HandleSeries(searchResults, imdb_id, kinopoisk_id, title, original_title, year, s, season, t, rjson, invoke, preferSeries: true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return await HandleMovie(searchResults, rjson, invoke);
|
return await HandleMovie(searchResults, rjson, invoke, preferSeries: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ActionResult> HandleSeries(List<SearchResult> searchResults, string imdb_id, long kinopoisk_id, string title, string original_title, int year, int s, int season, string t, bool rjson, UaTUTInvoke invoke)
|
private async Task<ActionResult> HandleSeries(List<SearchResult> searchResults, string imdb_id, long kinopoisk_id, string title, string original_title, int year, int s, int season, string t, bool rjson, UaTUTInvoke invoke, bool preferSeries)
|
||||||
{
|
{
|
||||||
var init = ModInit.UaTUT;
|
var init = ModInit.UaTUT;
|
||||||
|
|
||||||
// Фільтруємо тільки серіали та аніме
|
// Фільтруємо тільки серіали та аніме
|
||||||
var seriesResults = searchResults.Where(r => r.Category == "Серіал" || r.Category == "Аніме").ToList();
|
var seriesResults = searchResults.Where(r => IsSeriesCategory(r.Category, preferSeries)).ToList();
|
||||||
|
|
||||||
if (!seriesResults.Any())
|
if (!seriesResults.Any())
|
||||||
{
|
{
|
||||||
@ -113,8 +124,9 @@ namespace UaTUT
|
|||||||
{
|
{
|
||||||
var seasonItem = firstVoice.Seasons[i];
|
var seasonItem = firstVoice.Seasons[i];
|
||||||
string seasonName = seasonItem.Title ?? $"Сезон {i + 1}";
|
string seasonName = seasonItem.Title ?? $"Сезон {i + 1}";
|
||||||
string link = $"{host}/uatut?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&season={i}";
|
int seasonNumber = i + 1;
|
||||||
season_tpl.Append(seasonName, link, i.ToString());
|
string link = $"{host}/uatut?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&season={seasonNumber}";
|
||||||
|
season_tpl.Append(seasonName, link, seasonNumber.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
OnLog($"UaTUT: found {firstVoice.Seasons.Count} seasons");
|
OnLog($"UaTUT: found {firstVoice.Seasons.Count} seasons");
|
||||||
@ -137,8 +149,10 @@ namespace UaTUT
|
|||||||
if (playerData?.Voices == null || !playerData.Voices.Any())
|
if (playerData?.Voices == null || !playerData.Voices.Any())
|
||||||
return OnError();
|
return OnError();
|
||||||
|
|
||||||
|
int seasonIndex = season > 0 ? season - 1 : season;
|
||||||
|
|
||||||
// Перевіряємо чи існує вибраний сезон
|
// Перевіряємо чи існує вибраний сезон
|
||||||
if (season >= playerData.Voices.First().Seasons.Count)
|
if (seasonIndex >= playerData.Voices.First().Seasons.Count || seasonIndex < 0)
|
||||||
return OnError();
|
return OnError();
|
||||||
|
|
||||||
var voice_tpl = new VoiceTpl();
|
var voice_tpl = new VoiceTpl();
|
||||||
@ -156,7 +170,8 @@ namespace UaTUT
|
|||||||
{
|
{
|
||||||
var voice = playerData.Voices[i];
|
var voice = playerData.Voices[i];
|
||||||
string voiceName = voice.Name ?? $"Озвучка {i + 1}";
|
string voiceName = voice.Name ?? $"Озвучка {i + 1}";
|
||||||
string voiceLink = $"{host}/uatut?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&season={season}&t={i}";
|
int seasonNumber = seasonIndex + 1;
|
||||||
|
string voiceLink = $"{host}/uatut?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&season={seasonNumber}&t={i}";
|
||||||
bool isActive = selectedVoice == i.ToString();
|
bool isActive = selectedVoice == i.ToString();
|
||||||
voice_tpl.Append(voiceName, isActive, voiceLink);
|
voice_tpl.Append(voiceName, isActive, voiceLink);
|
||||||
}
|
}
|
||||||
@ -166,9 +181,9 @@ namespace UaTUT
|
|||||||
{
|
{
|
||||||
var selectedVoiceData = playerData.Voices[voiceIndex];
|
var selectedVoiceData = playerData.Voices[voiceIndex];
|
||||||
|
|
||||||
if (season < selectedVoiceData.Seasons.Count)
|
if (seasonIndex < selectedVoiceData.Seasons.Count)
|
||||||
{
|
{
|
||||||
var selectedSeason = selectedVoiceData.Seasons[season];
|
var selectedSeason = selectedVoiceData.Seasons[seasonIndex];
|
||||||
|
|
||||||
// Сортуємо епізоди та додаємо правильну нумерацію
|
// Сортуємо епізоди та додаємо правильну нумерацію
|
||||||
var sortedEpisodes = selectedSeason.Episodes.OrderBy(e => ExtractEpisodeNumber(e.Title)).ToList();
|
var sortedEpisodes = selectedSeason.Episodes.OrderBy(e => ExtractEpisodeNumber(e.Title)).ToList();
|
||||||
@ -181,11 +196,15 @@ namespace UaTUT
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(episodeFile))
|
if (!string.IsNullOrEmpty(episodeFile))
|
||||||
{
|
{
|
||||||
// Створюємо прямий лінк на епізод через play action
|
string streamUrl = BuildStreamUrl(init, episodeFile);
|
||||||
string episodeLink = $"{host}/uatut/play?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&s={s}&season={season}&t={selectedVoice}&episodeId={episode.Id}";
|
int seasonNumber = seasonIndex + 1;
|
||||||
|
episode_tpl.Append(
|
||||||
// Використовуємо правильний синтаксис EpisodeTpl.Append без poster параметра
|
episodeName,
|
||||||
episode_tpl.Append(episodeName, title ?? original_title, season.ToString(), (i + 1).ToString("D2"), episodeLink, "call");
|
title ?? original_title,
|
||||||
|
seasonNumber.ToString(),
|
||||||
|
(i + 1).ToString("D2"),
|
||||||
|
streamUrl
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -236,12 +255,12 @@ namespace UaTUT
|
|||||||
return match.Success ? int.Parse(match.Groups[1].Value) : 0;
|
return match.Success ? int.Parse(match.Groups[1].Value) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ActionResult> HandleMovie(List<SearchResult> searchResults, bool rjson, UaTUTInvoke invoke)
|
private async Task<ActionResult> HandleMovie(List<SearchResult> searchResults, bool rjson, UaTUTInvoke invoke, bool preferSeries)
|
||||||
{
|
{
|
||||||
var init = ModInit.UaTUT;
|
var init = ModInit.UaTUT;
|
||||||
|
|
||||||
// Фільтруємо тільки фільми
|
// Фільтруємо тільки фільми
|
||||||
var movieResults = searchResults.Where(r => r.Category == "Фільм").ToList();
|
var movieResults = searchResults.Where(r => IsMovieCategory(r.Category, preferSeries)).ToList();
|
||||||
|
|
||||||
if (!movieResults.Any())
|
if (!movieResults.Any())
|
||||||
{
|
{
|
||||||
@ -396,9 +415,10 @@ namespace UaTUT
|
|||||||
{
|
{
|
||||||
var selectedVoice = playerData.Voices[voiceIndex];
|
var selectedVoice = playerData.Voices[voiceIndex];
|
||||||
|
|
||||||
if (season >= 0 && season < selectedVoice.Seasons.Count)
|
int seasonIndex = season > 0 ? season - 1 : season;
|
||||||
|
if (seasonIndex >= 0 && seasonIndex < selectedVoice.Seasons.Count)
|
||||||
{
|
{
|
||||||
var selectedSeasonData = selectedVoice.Seasons[season];
|
var selectedSeasonData = selectedVoice.Seasons[seasonIndex];
|
||||||
|
|
||||||
foreach (var episode in selectedSeasonData.Episodes)
|
foreach (var episode in selectedSeasonData.Episodes)
|
||||||
{
|
{
|
||||||
@ -431,9 +451,65 @@ namespace UaTUT
|
|||||||
return OnError();
|
return OnError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 IsMovieCategory(string category, bool preferSeries)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(category))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var value = category.Trim().ToLowerInvariant();
|
||||||
|
if (IsAnimeCategory(value))
|
||||||
|
return !preferSeries;
|
||||||
|
|
||||||
|
return value == "фільм" || value == "фильм" || value == "мультфільм" || value == "мультфильм" || value == "movie";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSeriesCategory(string category, bool preferSeries)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(category))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var value = category.Trim().ToLowerInvariant();
|
||||||
|
if (IsAnimeCategory(value))
|
||||||
|
return preferSeries;
|
||||||
|
|
||||||
|
return value == "серіал" || value == "сериал"
|
||||||
|
|| value == "аніме" || value == "аниме"
|
||||||
|
|| value == "мультсеріал" || value == "мультсериал"
|
||||||
|
|| value == "tv";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAnimeCategory(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return value == "аніме" || value == "аниме";
|
||||||
|
}
|
||||||
|
|
||||||
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
||||||
{
|
{
|
||||||
string link = accsArgs(streamLink);
|
string link = streamLink?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(link))
|
||||||
|
return link;
|
||||||
|
|
||||||
|
link = StripLampacArgs(link);
|
||||||
|
|
||||||
if (ApnHelper.IsEnabled(init))
|
if (ApnHelper.IsEnabled(init))
|
||||||
{
|
{
|
||||||
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
||||||
|
|||||||
@ -24,7 +24,7 @@ namespace UaTUT
|
|||||||
{
|
{
|
||||||
public class ModInit
|
public class ModInit
|
||||||
{
|
{
|
||||||
public static double Version => 3.3;
|
public static double Version => 3.5;
|
||||||
|
|
||||||
public static OnlinesSettings UaTUT;
|
public static OnlinesSettings UaTUT;
|
||||||
public static bool ApnHostProvided;
|
public static bool ApnHostProvided;
|
||||||
|
|||||||
@ -47,6 +47,9 @@ namespace Uaflix.Controllers
|
|||||||
// Обробка параметра checksearch - повертаємо спеціальну відповідь для валідації
|
// Обробка параметра checksearch - повертаємо спеціальну відповідь для валідації
|
||||||
if (checksearch)
|
if (checksearch)
|
||||||
{
|
{
|
||||||
|
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||||
|
return OnError("uaflix", proxyManager);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string filmTitle = !string.IsNullOrEmpty(title) ? title : original_title;
|
string filmTitle = !string.IsNullOrEmpty(title) ? title : original_title;
|
||||||
@ -167,11 +170,22 @@ namespace Uaflix.Controllers
|
|||||||
// s == -1: Вибір сезону
|
// s == -1: Вибір сезону
|
||||||
if (s == -1)
|
if (s == -1)
|
||||||
{
|
{
|
||||||
var allSeasons = structure.Voices
|
List<int> allSeasons;
|
||||||
.SelectMany(v => v.Value.Seasons.Keys)
|
VoiceInfo tVoice = null;
|
||||||
.Distinct()
|
bool restrictByVoice = !string.IsNullOrEmpty(t) && structure.Voices.TryGetValue(t, out tVoice) && IsAshdiVoice(tVoice);
|
||||||
.OrderBy(sn => sn)
|
if (restrictByVoice)
|
||||||
.ToList();
|
{
|
||||||
|
allSeasons = GetSeasonSet(tVoice).OrderBy(sn => sn).ToList();
|
||||||
|
OnLog($"Ashdi voice selected (t='{t}'), seasons count={allSeasons.Count}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
allSeasons = structure.Voices
|
||||||
|
.SelectMany(v => GetSeasonSet(v.Value))
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(sn => sn)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
OnLog($"Found {allSeasons.Count} seasons in structure: {string.Join(", ", allSeasons)}");
|
OnLog($"Found {allSeasons.Count} seasons in structure: {string.Join(", ", allSeasons)}");
|
||||||
|
|
||||||
@ -205,6 +219,8 @@ namespace Uaflix.Controllers
|
|||||||
foreach (var season in seasonsWithValidEpisodes)
|
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)}";
|
string link = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season}&href={HttpUtility.UrlEncode(filmUrl)}";
|
||||||
|
if (restrictByVoice)
|
||||||
|
link += $"&t={HttpUtility.UrlEncode(t)}";
|
||||||
season_tpl.Append($"{season}", link, season.ToString());
|
season_tpl.Append($"{season}", link, season.ToString());
|
||||||
OnLog($"Added season {season} to template");
|
OnLog($"Added season {season} to template");
|
||||||
}
|
}
|
||||||
@ -239,12 +255,31 @@ namespace Uaflix.Controllers
|
|||||||
t = voicesForSeason[0].DisplayName;
|
t = voicesForSeason[0].DisplayName;
|
||||||
OnLog($"Auto-selected first voice: {t}");
|
OnLog($"Auto-selected first voice: {t}");
|
||||||
}
|
}
|
||||||
|
else if (!structure.Voices.ContainsKey(t))
|
||||||
|
{
|
||||||
|
t = voicesForSeason[0].DisplayName;
|
||||||
|
OnLog($"Voice '{t}' not found, fallback to first voice: {t}");
|
||||||
|
}
|
||||||
|
|
||||||
// Створюємо VoiceTpl з усіма озвучками
|
// Створюємо VoiceTpl з усіма озвучками
|
||||||
var voice_tpl = new VoiceTpl();
|
var voice_tpl = new VoiceTpl();
|
||||||
|
var selectedVoiceInfo = structure.Voices[t];
|
||||||
|
var selectedSeasonSet = GetSeasonSet(selectedVoiceInfo);
|
||||||
|
bool selectedIsAshdi = IsAshdiVoice(selectedVoiceInfo);
|
||||||
|
|
||||||
foreach (var voice in voicesForSeason)
|
foreach (var voice in voicesForSeason)
|
||||||
{
|
{
|
||||||
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&s={s}&t={HttpUtility.UrlEncode(voice.DisplayName)}&href={HttpUtility.UrlEncode(filmUrl)}";
|
bool targetIsAshdi = IsAshdiVoice(voice.Info);
|
||||||
|
var targetSeasonSet = GetSeasonSet(voice.Info);
|
||||||
|
bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet);
|
||||||
|
bool needSeasonReset = (selectedIsAshdi || targetIsAshdi) && !sameSeasonSet;
|
||||||
|
|
||||||
|
string voiceLink = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&href={HttpUtility.UrlEncode(filmUrl)}";
|
||||||
|
if (needSeasonReset)
|
||||||
|
voiceLink += $"&s=-1&t={HttpUtility.UrlEncode(voice.DisplayName)}";
|
||||||
|
else
|
||||||
|
voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.DisplayName)}";
|
||||||
|
|
||||||
bool isActive = voice.DisplayName == t;
|
bool isActive = voice.DisplayName == t;
|
||||||
voice_tpl.Append(voice.DisplayName, isActive, voiceLink);
|
voice_tpl.Append(voice.DisplayName, isActive, voiceLink);
|
||||||
}
|
}
|
||||||
@ -261,6 +296,13 @@ namespace Uaflix.Controllers
|
|||||||
if (!structure.Voices[t].Seasons.ContainsKey(s))
|
if (!structure.Voices[t].Seasons.ContainsKey(s))
|
||||||
{
|
{
|
||||||
OnLog($"Season {s} not found for voice '{t}'");
|
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 ===");
|
OnLog("=== RETURN: season not found for voice OnError ===");
|
||||||
return OnError("uaflix", proxyManager);
|
return OnError("uaflix", proxyManager);
|
||||||
}
|
}
|
||||||
@ -336,7 +378,10 @@ namespace Uaflix.Controllers
|
|||||||
|
|
||||||
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
||||||
{
|
{
|
||||||
string link = accsArgs(streamLink);
|
string link = StripLampacArgs(streamLink?.Trim());
|
||||||
|
if (string.IsNullOrEmpty(link))
|
||||||
|
return link;
|
||||||
|
|
||||||
if (ApnHelper.IsEnabled(init))
|
if (ApnHelper.IsEnabled(init))
|
||||||
{
|
{
|
||||||
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
|
||||||
@ -350,5 +395,40 @@ namespace Uaflix.Controllers
|
|||||||
|
|
||||||
return HostStreamProxy(init, link);
|
return HostStreamProxy(init, link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 IsAshdiVoice(VoiceInfo voice)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (voice?.Seasons == null || voice.Seasons.Count == 0)
|
||||||
|
return new HashSet<int>();
|
||||||
|
|
||||||
|
return voice.Seasons
|
||||||
|
.Where(kv => kv.Value != null && kv.Value.Any(ep => !string.IsNullOrEmpty(ep.File)))
|
||||||
|
.Select(kv => kv.Key)
|
||||||
|
.ToHashSet();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ namespace Uaflix
|
|||||||
{
|
{
|
||||||
public class ModInit
|
public class ModInit
|
||||||
{
|
{
|
||||||
public static double Version => 3.3;
|
public static double Version => 3.5;
|
||||||
|
|
||||||
public static OnlinesSettings UaFlix;
|
public static OnlinesSettings UaFlix;
|
||||||
public static bool ApnHostProvided;
|
public static bool ApnHostProvided;
|
||||||
|
|||||||
@ -23,7 +23,7 @@ namespace Unimay.Controllers
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("unimay")]
|
[Route("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)
|
async public ValueTask<ActionResult> Index(string title, string original_title, string code, int serial = -1, int s = -1, int e = -1, bool play = false, bool rjson = false, bool checksearch = false)
|
||||||
{
|
{
|
||||||
await UpdateService.ConnectAsync(host);
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
@ -33,6 +33,18 @@ namespace Unimay.Controllers
|
|||||||
|
|
||||||
var invoke = new UnimayInvoke(init, hybridCache, OnLog, proxyManager);
|
var invoke = new UnimayInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
|
||||||
|
if (checksearch)
|
||||||
|
{
|
||||||
|
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||||
|
return OnError("unimay");
|
||||||
|
|
||||||
|
var searchResults = await invoke.Search(title, original_title, serial);
|
||||||
|
if (searchResults?.Content != null && searchResults.Content.Count > 0)
|
||||||
|
return Content("data-json=", "text/plain; charset=utf-8");
|
||||||
|
|
||||||
|
return OnError("unimay");
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(code))
|
if (!string.IsNullOrEmpty(code))
|
||||||
{
|
{
|
||||||
// Fetch release details
|
// Fetch release details
|
||||||
@ -104,7 +116,8 @@ namespace Unimay.Controllers
|
|||||||
if (string.IsNullOrEmpty(masterUrl))
|
if (string.IsNullOrEmpty(masterUrl))
|
||||||
return OnError("no stream");
|
return OnError("no stream");
|
||||||
|
|
||||||
return UpdateService.Validate(Redirect(HostStreamProxy(init, accsArgs(masterUrl), proxy: proxyManager.Get())));
|
string cleaned = StripLampacArgs(masterUrl?.Trim());
|
||||||
|
return UpdateService.Validate(Redirect(HostStreamProxy(init, cleaned, proxy: proxyManager.Get())));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemType == "Фільм")
|
if (itemType == "Фільм")
|
||||||
@ -140,5 +153,21 @@ namespace Unimay.Controllers
|
|||||||
return OnError("unsupported type");
|
return OnError("unsupported type");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ namespace Unimay
|
|||||||
{
|
{
|
||||||
public class ModInit
|
public class ModInit
|
||||||
{
|
{
|
||||||
public static double Version => 3.2;
|
public static double Version => 3.3;
|
||||||
|
|
||||||
public static OnlinesSettings Unimay;
|
public static OnlinesSettings Unimay;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user