mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-04-16 17:32:20 +00:00
Compare commits
No commits in common. "6cdddcbf309ca7ac7b7a42856f677d29572e7f94" and "56eb95be3d6b4f904c97e105aef1576314490383" have entirely different histories.
6cdddcbf30
...
56eb95be3d
@ -479,6 +479,7 @@ namespace Makhno
|
|||||||
}
|
}
|
||||||
|
|
||||||
string searchQuery = originalTitle ?? title;
|
string searchQuery = originalTitle ?? title;
|
||||||
|
|
||||||
string klonSearchCacheKey = $"makhno:klonfun:search:{imdbId ?? searchQuery}";
|
string klonSearchCacheKey = $"makhno:klonfun:search:{imdbId ?? searchQuery}";
|
||||||
var klonSearchResults = await InvokeCache<List<KlonSearchResult>>(klonSearchCacheKey, TimeSpan.FromMinutes(10), async () =>
|
var klonSearchResults = await InvokeCache<List<KlonSearchResult>>(klonSearchCacheKey, TimeSpan.FromMinutes(10), async () =>
|
||||||
{
|
{
|
||||||
@ -519,7 +520,60 @@ namespace Makhno
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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)
|
private bool IsSerialByUrl(string url, int serial)
|
||||||
|
|||||||
@ -316,6 +316,140 @@ namespace Makhno
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(_init.cors(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(_init.cors(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;
|
||||||
|
}
|
||||||
|
|
||||||
private static string NormalizeUrl(string host, string url)
|
private static string NormalizeUrl(string host, string url)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(url))
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
@ -836,6 +970,64 @@ namespace Makhno
|
|||||||
return $"{AshdiHost}/{path.TrimStart('/')}";
|
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 YearMatch(KlonSearchResult item, int? year)
|
private bool YearMatch(KlonSearchResult item, int? year)
|
||||||
{
|
{
|
||||||
if (year == null || item == null || item.Year <= 0)
|
if (year == null || item == null || item.Year <= 0)
|
||||||
@ -844,6 +1036,14 @@ namespace Makhno
|
|||||||
return item.Year == year.Value;
|
return item.Year == year.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TitleMatch(SearchResult item, string title, string titleEn)
|
||||||
|
{
|
||||||
|
if (item == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return TitleMatch(item.Title, item.TitleEn, title, titleEn);
|
||||||
|
}
|
||||||
|
|
||||||
private bool TitleMatch(string itemTitle, string title, string titleEn)
|
private bool TitleMatch(string itemTitle, string title, string titleEn)
|
||||||
{
|
{
|
||||||
return TitleMatch(itemTitle, null, title, titleEn);
|
return TitleMatch(itemTitle, null, title, titleEn);
|
||||||
@ -876,6 +1076,17 @@ namespace Makhno
|
|||||||
return text.Trim();
|
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, bool isSerial)
|
public async Task<(JObject item, string mediaType)?> FetchTmdbByImdb(string imdbId, int? year, bool isSerial)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(imdbId))
|
if (string.IsNullOrWhiteSpace(imdbId))
|
||||||
|
|||||||
@ -43,6 +43,7 @@ namespace Makhno
|
|||||||
{
|
{
|
||||||
displayname = "Махно",
|
displayname = "Махно",
|
||||||
displayindex = 0,
|
displayindex = 0,
|
||||||
|
apihost = "https://uk.uatut.fun/watch",
|
||||||
proxy = new Shared.Models.Base.ProxySettings()
|
proxy = new Shared.Models.Base.ProxySettings()
|
||||||
{
|
{
|
||||||
useAuth = true,
|
useAuth = true,
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
### TVShows and Movies
|
### TVShows and Movies
|
||||||
|
|
||||||
- [x] UAFlix
|
- [x] UAFlix
|
||||||
|
- [x] UATuTFun
|
||||||
- [x] Makhno
|
- [x] Makhno
|
||||||
- [x] StarLight
|
- [x] StarLight
|
||||||
- [x] KlonFUN
|
- [x] KlonFUN
|
||||||
@ -41,7 +42,9 @@ Create or update the module/repository.yaml file
|
|||||||
- AnimeON
|
- AnimeON
|
||||||
- Unimay
|
- Unimay
|
||||||
- Mikai
|
- Mikai
|
||||||
|
- UATuT
|
||||||
- Uaflix
|
- Uaflix
|
||||||
|
- UaTUT
|
||||||
- Bamboo
|
- Bamboo
|
||||||
- Makhno
|
- Makhno
|
||||||
- StarLight
|
- StarLight
|
||||||
@ -89,6 +92,7 @@ Parameter compatibility:
|
|||||||
Sources with APN support:
|
Sources with APN support:
|
||||||
- AnimeON
|
- AnimeON
|
||||||
- Uaflix
|
- Uaflix
|
||||||
|
- UaTUT
|
||||||
- Mikai
|
- Mikai
|
||||||
- Makhno
|
- Makhno
|
||||||
- KlonFUN
|
- KlonFUN
|
||||||
|
|||||||
86
UaTUT/ApnHelper.cs
Normal file
86
UaTUT/ApnHelper.cs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Shared.Models.Base;
|
||||||
|
using System;
|
||||||
|
using System.Web;
|
||||||
|
|
||||||
|
namespace Shared.Engine
|
||||||
|
{
|
||||||
|
public static class ApnHelper
|
||||||
|
{
|
||||||
|
public const string DefaultHost = "https://tut.im/proxy.php?url={encodeurl}";
|
||||||
|
|
||||||
|
public static bool TryGetInitConf(JObject conf, out bool enabled, out string host)
|
||||||
|
{
|
||||||
|
enabled = false;
|
||||||
|
host = null;
|
||||||
|
|
||||||
|
if (conf == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!conf.TryGetValue("apn", out var apnToken) || apnToken?.Type != JTokenType.Boolean)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
enabled = apnToken.Value<bool>();
|
||||||
|
host = conf.Value<string>("apn_host");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
|
||||||
|
{
|
||||||
|
if (init == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
init.apnstream = false;
|
||||||
|
init.apn = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
550
UaTUT/Controller.cs
Normal file
550
UaTUT/Controller.cs
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
using Shared.Engine;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Web;
|
||||||
|
using System.Linq;
|
||||||
|
using Shared;
|
||||||
|
using Shared.Models.Templates;
|
||||||
|
using UaTUT.Models;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Shared.Models.Online.Settings;
|
||||||
|
using Shared.Models;
|
||||||
|
|
||||||
|
namespace UaTUT
|
||||||
|
{
|
||||||
|
[Route("uatut")]
|
||||||
|
public class UaTUTController : BaseOnlineController
|
||||||
|
{
|
||||||
|
ProxyManager proxyManager;
|
||||||
|
|
||||||
|
public UaTUTController() : base(ModInit.Settings)
|
||||||
|
{
|
||||||
|
proxyManager = new ProxyManager(ModInit.UaTUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[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, bool checksearch = false)
|
||||||
|
{
|
||||||
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
|
var init = await loadKit(ModInit.UaTUT);
|
||||||
|
if (!init.enable)
|
||||||
|
return OnError();
|
||||||
|
Initialization(init);
|
||||||
|
|
||||||
|
OnLog($"UaTUT: {title} (serial={serial}, s={s}, season={season}, t={t})");
|
||||||
|
|
||||||
|
var invoke = new UaTUTInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
|
||||||
|
// Використовуємо кеш для пошуку, щоб уникнути дублювання запитів
|
||||||
|
string searchCacheKey = $"uatut:search:{imdb_id ?? original_title ?? title}";
|
||||||
|
var searchResults = await InvokeCache<List<SearchResult>>(searchCacheKey, TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
OnLog("UaTUT: No search results found");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serial == 1)
|
||||||
|
{
|
||||||
|
return await HandleSeries(searchResults, imdb_id, kinopoisk_id, title, original_title, year, s, season, t, rjson, invoke, preferSeries: true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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, bool preferSeries)
|
||||||
|
{
|
||||||
|
var init = ModInit.UaTUT;
|
||||||
|
|
||||||
|
// Фільтруємо тільки серіали та аніме
|
||||||
|
var seriesResults = searchResults.Where(r => IsSeriesCategory(r.Category, preferSeries)).ToList();
|
||||||
|
|
||||||
|
if (!seriesResults.Any())
|
||||||
|
{
|
||||||
|
OnLog("UaTUT: No series found in search results");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s == -1) // Крок 1: Відображення списку серіалів
|
||||||
|
{
|
||||||
|
var season_tpl = new SeasonTpl();
|
||||||
|
for (int i = 0; i < seriesResults.Count; i++)
|
||||||
|
{
|
||||||
|
var series = seriesResults[i];
|
||||||
|
string seasonName = $"{series.Title} ({series.Year})";
|
||||||
|
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={i}";
|
||||||
|
season_tpl.Append(seasonName, link, i.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
OnLog($"UaTUT: generated {seriesResults.Count} series options");
|
||||||
|
return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
else if (season == -1) // Крок 2: Відображення сезонів для вибраного серіалу
|
||||||
|
{
|
||||||
|
if (s >= seriesResults.Count)
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
var selectedSeries = seriesResults[s];
|
||||||
|
|
||||||
|
// Використовуємо кеш для уникнення повторних запитів
|
||||||
|
string cacheKey = $"uatut:player_data:{selectedSeries.Id}";
|
||||||
|
var playerData = await InvokeCache<PlayerData>(cacheKey, TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
return await GetPlayerDataCached(selectedSeries, invoke);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (playerData?.Voices == null || !playerData.Voices.Any())
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
// Використовуємо першу озвучку для отримання списку сезонів
|
||||||
|
var firstVoice = playerData.Voices.First();
|
||||||
|
|
||||||
|
var season_tpl = new SeasonTpl();
|
||||||
|
for (int i = 0; i < firstVoice.Seasons.Count; i++)
|
||||||
|
{
|
||||||
|
var seasonItem = firstVoice.Seasons[i];
|
||||||
|
string seasonName = seasonItem.Title ?? $"Сезон {i + 1}";
|
||||||
|
int seasonNumber = 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={seasonNumber}";
|
||||||
|
season_tpl.Append(seasonName, link, seasonNumber.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
OnLog($"UaTUT: found {firstVoice.Seasons.Count} seasons");
|
||||||
|
return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
else // Крок 3: Відображення озвучок та епізодів для вибраного сезону
|
||||||
|
{
|
||||||
|
if (s >= seriesResults.Count)
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
var selectedSeries = seriesResults[s];
|
||||||
|
|
||||||
|
// Використовуємо той самий кеш
|
||||||
|
string cacheKey = $"uatut:player_data:{selectedSeries.Id}";
|
||||||
|
var playerData = await InvokeCache<PlayerData>(cacheKey, TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
return await GetPlayerDataCached(selectedSeries, invoke);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (playerData?.Voices == null || !playerData.Voices.Any())
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
int seasonIndex = season > 0 ? season - 1 : season;
|
||||||
|
|
||||||
|
// Перевіряємо чи існує вибраний сезон
|
||||||
|
if (seasonIndex >= playerData.Voices.First().Seasons.Count || seasonIndex < 0)
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
var voice_tpl = new VoiceTpl();
|
||||||
|
var episode_tpl = new EpisodeTpl();
|
||||||
|
|
||||||
|
// Автоматично вибираємо першу озвучку якщо не вибрана
|
||||||
|
string selectedVoice = t;
|
||||||
|
if (string.IsNullOrEmpty(selectedVoice) && playerData.Voices.Any())
|
||||||
|
{
|
||||||
|
selectedVoice = "0"; // Перша озвучка
|
||||||
|
}
|
||||||
|
|
||||||
|
// Додаємо всі озвучки
|
||||||
|
for (int i = 0; i < playerData.Voices.Count; i++)
|
||||||
|
{
|
||||||
|
var voice = playerData.Voices[i];
|
||||||
|
string voiceName = voice.Name ?? $"Озвучка {i + 1}";
|
||||||
|
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();
|
||||||
|
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];
|
||||||
|
|
||||||
|
if (seasonIndex < selectedVoiceData.Seasons.Count)
|
||||||
|
{
|
||||||
|
var selectedSeason = selectedVoiceData.Seasons[seasonIndex];
|
||||||
|
|
||||||
|
// Сортуємо епізоди та додаємо правильну нумерацію
|
||||||
|
var sortedEpisodes = selectedSeason.Episodes.OrderBy(e => ExtractEpisodeNumber(e.Title)).ToList();
|
||||||
|
|
||||||
|
for (int i = 0; i < sortedEpisodes.Count; i++)
|
||||||
|
{
|
||||||
|
var episode = sortedEpisodes[i];
|
||||||
|
string episodeName = episode.Title;
|
||||||
|
string episodeFile = episode.File;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(episodeFile))
|
||||||
|
{
|
||||||
|
string streamUrl = BuildStreamUrl(init, episodeFile);
|
||||||
|
int seasonNumber = seasonIndex + 1;
|
||||||
|
episode_tpl.Append(
|
||||||
|
episodeName,
|
||||||
|
title ?? original_title,
|
||||||
|
seasonNumber.ToString(),
|
||||||
|
(i + 1).ToString("D2"),
|
||||||
|
streamUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int voiceCount = playerData.Voices.Count;
|
||||||
|
int episodeCount = 0;
|
||||||
|
if (!string.IsNullOrEmpty(selectedVoice) && int.TryParse(selectedVoice, out int vIndex) && vIndex < playerData.Voices.Count)
|
||||||
|
{
|
||||||
|
var selectedVoiceData = playerData.Voices[vIndex];
|
||||||
|
if (season < selectedVoiceData.Seasons.Count)
|
||||||
|
{
|
||||||
|
episodeCount = selectedVoiceData.Seasons[season].Episodes.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnLog($"UaTUT: generated {voiceCount} voices, {episodeCount} episodes");
|
||||||
|
|
||||||
|
episode_tpl.Append(voice_tpl);
|
||||||
|
if (rjson)
|
||||||
|
return Content(episode_tpl.ToJson(), "application/json; charset=utf-8");
|
||||||
|
|
||||||
|
return Content(episode_tpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Допоміжний метод для кешованого отримання даних плеєра
|
||||||
|
private async Task<PlayerData> GetPlayerDataCached(SearchResult selectedSeries, UaTUTInvoke invoke)
|
||||||
|
{
|
||||||
|
var pageContent = await invoke.GetMoviePageContent(selectedSeries.Id);
|
||||||
|
if (string.IsNullOrEmpty(pageContent))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var playerUrl = await invoke.GetPlayerUrl(pageContent);
|
||||||
|
if (string.IsNullOrEmpty(playerUrl))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return await invoke.GetPlayerData(playerUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Допоміжний метод для витягування номера епізоду з назви
|
||||||
|
private int ExtractEpisodeNumber(string title)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(title))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var match = Regex.Match(title, @"(\d+)");
|
||||||
|
return match.Success ? int.Parse(match.Groups[1].Value) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ActionResult> HandleMovie(List<SearchResult> searchResults, bool rjson, UaTUTInvoke invoke, bool preferSeries)
|
||||||
|
{
|
||||||
|
var init = ModInit.UaTUT;
|
||||||
|
|
||||||
|
// Фільтруємо тільки фільми
|
||||||
|
var movieResults = searchResults.Where(r => IsMovieCategory(r.Category, preferSeries)).ToList();
|
||||||
|
|
||||||
|
if (!movieResults.Any())
|
||||||
|
{
|
||||||
|
OnLog("UaTUT: No movies found in search results");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
var movie_tpl = new MovieTpl(title: "UaTUT Movies", original_title: "UaTUT Movies");
|
||||||
|
|
||||||
|
foreach (var movie in movieResults)
|
||||||
|
{
|
||||||
|
var pageContent = await invoke.GetMoviePageContent(movie.Id);
|
||||||
|
if (string.IsNullOrEmpty(pageContent))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var playerUrl = await invoke.GetPlayerUrl(pageContent);
|
||||||
|
if (string.IsNullOrEmpty(playerUrl))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var playerData = await invoke.GetPlayerData(playerUrl);
|
||||||
|
var movieStreams = playerData?.Movies?
|
||||||
|
.Where(m => m != null && !string.IsNullOrEmpty(m.File))
|
||||||
|
.ToList() ?? new List<MovieVariant>();
|
||||||
|
|
||||||
|
if (movieStreams.Count == 0 && !string.IsNullOrEmpty(playerData?.File))
|
||||||
|
{
|
||||||
|
movieStreams.Add(new MovieVariant
|
||||||
|
{
|
||||||
|
File = playerData.File,
|
||||||
|
Title = "Основне джерело",
|
||||||
|
Quality = "auto"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movieStreams.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var variant in movieStreams)
|
||||||
|
{
|
||||||
|
string label = !string.IsNullOrWhiteSpace(variant.Title)
|
||||||
|
? variant.Title
|
||||||
|
: "Варіант";
|
||||||
|
|
||||||
|
movie_tpl.Append(label, BuildStreamUrl(init, variant.File));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movie_tpl.data == null || movie_tpl.data.Count == 0)
|
||||||
|
{
|
||||||
|
OnLog("UaTUT: No playable movies found");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
OnLog($"UaTUT: found {movieResults.Count} movies");
|
||||||
|
return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("play/movie")]
|
||||||
|
async public Task<ActionResult> PlayMovie(long imdb_id, string title, int year, string stream = null, bool play = false, bool rjson = false)
|
||||||
|
{
|
||||||
|
await UpdateService.ConnectAsync(host);
|
||||||
|
|
||||||
|
var init = await loadKit(ModInit.UaTUT);
|
||||||
|
if (!init.enable)
|
||||||
|
return OnError();
|
||||||
|
Initialization(init);
|
||||||
|
|
||||||
|
OnLog($"UaTUT PlayMovie: {title} ({year}) play={play}");
|
||||||
|
|
||||||
|
var invoke = new UaTUTInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
|
||||||
|
// Використовуємо кеш для пошуку
|
||||||
|
string searchCacheKey = $"uatut:search:{title}";
|
||||||
|
var searchResults = await InvokeCache<List<SearchResult>>(searchCacheKey, TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
return await invoke.Search(title, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchResults == null || !searchResults.Any())
|
||||||
|
{
|
||||||
|
OnLog("UaTUT PlayMovie: No search results found");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Шукаємо фільм за ID
|
||||||
|
var movie = searchResults.FirstOrDefault(r => r.Id == imdb_id.ToString() && r.Category == "Фільм");
|
||||||
|
if (movie == null)
|
||||||
|
{
|
||||||
|
OnLog("UaTUT PlayMovie: Movie not found");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
var pageContent = await invoke.GetMoviePageContent(movie.Id);
|
||||||
|
if (string.IsNullOrEmpty(pageContent))
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
var playerUrl = await invoke.GetPlayerUrl(pageContent);
|
||||||
|
if (string.IsNullOrEmpty(playerUrl))
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
var playerData = await invoke.GetPlayerData(playerUrl);
|
||||||
|
string selectedFile = HttpUtility.UrlDecode(stream);
|
||||||
|
if (string.IsNullOrWhiteSpace(selectedFile))
|
||||||
|
selectedFile = playerData?.Movies?.FirstOrDefault(m => !string.IsNullOrWhiteSpace(m.File))?.File ?? playerData?.File;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(selectedFile))
|
||||||
|
return OnError();
|
||||||
|
|
||||||
|
OnLog($"UaTUT PlayMovie: обрано потік {selectedFile}");
|
||||||
|
|
||||||
|
string streamUrl = BuildStreamUrl(init, selectedFile);
|
||||||
|
|
||||||
|
// Якщо play=true, робимо Redirect, інакше повертаємо JSON
|
||||||
|
if (play)
|
||||||
|
return UpdateService.Validate(Redirect(streamUrl));
|
||||||
|
else
|
||||||
|
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title), "application/json; charset=utf-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("play")]
|
||||||
|
async public 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.UaTUT);
|
||||||
|
if (!init.enable)
|
||||||
|
return OnError();
|
||||||
|
Initialization(init);
|
||||||
|
|
||||||
|
OnLog($"UaTUT Play: {title} (s={s}, season={season}, t={t}, episodeId={episodeId}) play={play}");
|
||||||
|
|
||||||
|
var invoke = new UaTUTInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
|
||||||
|
// Використовуємо кеш для пошуку
|
||||||
|
string searchCacheKey = $"uatut:search:{imdb_id ?? original_title ?? title}";
|
||||||
|
var searchResults = await InvokeCache<List<SearchResult>>(searchCacheKey, TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
return await invoke.Search(original_title ?? title, imdb_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchResults == null || !searchResults.Any())
|
||||||
|
{
|
||||||
|
OnLog("UaTUT Play: No search results found");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фільтруємо тільки серіали та аніме
|
||||||
|
var seriesResults = searchResults.Where(r => r.Category == "Серіал" || r.Category == "Аніме").ToList();
|
||||||
|
|
||||||
|
if (!seriesResults.Any() || s >= seriesResults.Count)
|
||||||
|
{
|
||||||
|
OnLog("UaTUT Play: No series found or invalid series index");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedSeries = seriesResults[s];
|
||||||
|
|
||||||
|
// Використовуємо той самий кеш як і в HandleSeries
|
||||||
|
string cacheKey = $"uatut:player_data:{selectedSeries.Id}";
|
||||||
|
var playerData = await InvokeCache<PlayerData>(cacheKey, TimeSpan.FromMinutes(10), async () =>
|
||||||
|
{
|
||||||
|
return await GetPlayerDataCached(selectedSeries, invoke);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (playerData?.Voices == null || !playerData.Voices.Any())
|
||||||
|
{
|
||||||
|
OnLog("UaTUT Play: No player data or voices found");
|
||||||
|
return OnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Знаходимо потрібний епізод в конкретному сезоні та озвучці
|
||||||
|
if (int.TryParse(t, out int voiceIndex) && voiceIndex < playerData.Voices.Count)
|
||||||
|
{
|
||||||
|
var selectedVoice = playerData.Voices[voiceIndex];
|
||||||
|
|
||||||
|
int seasonIndex = season > 0 ? season - 1 : season;
|
||||||
|
if (seasonIndex >= 0 && seasonIndex < selectedVoice.Seasons.Count)
|
||||||
|
{
|
||||||
|
var selectedSeasonData = selectedVoice.Seasons[seasonIndex];
|
||||||
|
|
||||||
|
foreach (var episode in selectedSeasonData.Episodes)
|
||||||
|
{
|
||||||
|
if (episode.Id == episodeId && !string.IsNullOrEmpty(episode.File))
|
||||||
|
{
|
||||||
|
OnLog($"UaTUT Play: Found episode {episode.Title}, stream: {episode.File}");
|
||||||
|
|
||||||
|
string streamUrl = BuildStreamUrl(init, episode.File);
|
||||||
|
string episodeTitle = $"{title ?? original_title} - {episode.Title}";
|
||||||
|
|
||||||
|
// Якщо play=true, робимо Redirect, інакше повертаємо JSON
|
||||||
|
if (play)
|
||||||
|
return UpdateService.Validate(Redirect(streamUrl));
|
||||||
|
else
|
||||||
|
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, episodeTitle), "application/json; charset=utf-8"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnLog($"UaTUT Play: Invalid season {season}, available seasons: {selectedVoice.Seasons.Count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnLog($"UaTUT Play: Invalid voice index {t}, available voices: {playerData.Voices.Count}");
|
||||||
|
}
|
||||||
|
|
||||||
|
OnLog("UaTUT Play: Episode not found");
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
199
UaTUT/ModInit.cs
Normal file
199
UaTUT/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 UaTUT
|
||||||
|
{
|
||||||
|
public class ModInit
|
||||||
|
{
|
||||||
|
public static double Version => 3.7;
|
||||||
|
|
||||||
|
public static OnlinesSettings UaTUT;
|
||||||
|
public static bool ApnHostProvided;
|
||||||
|
|
||||||
|
public static OnlinesSettings Settings
|
||||||
|
{
|
||||||
|
get => UaTUT;
|
||||||
|
set => UaTUT = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// модуль загружен
|
||||||
|
/// </summary>
|
||||||
|
public static void loaded(InitspaceModel initspace)
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
UaTUT = new OnlinesSettings("UaTUT", "https://uk.uatut.fun", streamproxy: false, useproxy: false)
|
||||||
|
{
|
||||||
|
displayname = "🇺🇦 UaTUT",
|
||||||
|
displayindex = 0,
|
||||||
|
apihost = "https://uk.uatut.fun/watch",
|
||||||
|
proxy = new Shared.Models.Base.ProxySettings()
|
||||||
|
{
|
||||||
|
useAuth = true,
|
||||||
|
username = "a",
|
||||||
|
password = "a",
|
||||||
|
list = new string[] { "socks5://IP:PORT" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var conf = ModuleInvoke.Conf("UaTUT", UaTUT);
|
||||||
|
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||||
|
conf.Remove("apn");
|
||||||
|
conf.Remove("apn_host");
|
||||||
|
UaTUT = conf.ToObject<OnlinesSettings>();
|
||||||
|
if (hasApn)
|
||||||
|
ApnHelper.ApplyInitConf(apnEnabled, apnHost, UaTUT);
|
||||||
|
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
|
||||||
|
if (hasApn && apnEnabled)
|
||||||
|
{
|
||||||
|
UaTUT.streamproxy = false;
|
||||||
|
}
|
||||||
|
else if (UaTUT.streamproxy)
|
||||||
|
{
|
||||||
|
UaTUT.apnstream = false;
|
||||||
|
UaTUT.apn = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Виводити "уточнити пошук"
|
||||||
|
AppInit.conf.online.with_search.Add("uatut");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 4))
|
||||||
|
: DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
ResetConnectTime(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ResetConnectTime(object? state)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_connectTime = null;
|
||||||
|
Connect = null;
|
||||||
|
|
||||||
|
_resetTimer?.Dispose();
|
||||||
|
_resetTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static bool IsDisconnected()
|
||||||
|
{
|
||||||
|
return _disconnectTime is not null
|
||||||
|
&& DateTime.UtcNow >= _disconnectTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ActionResult Validate(ActionResult result)
|
||||||
|
{
|
||||||
|
return IsDisconnected()
|
||||||
|
? throw new JsonReaderException($"Disconnect error: {Guid.CreateVersion7()}")
|
||||||
|
: result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ConnectResponse(bool IsUpdateUnavailable, bool IsNoiseEnabled);
|
||||||
|
}
|
||||||
69
UaTUT/Models/UaTUTModels.cs
Normal file
69
UaTUT/Models/UaTUTModels.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace UaTUT.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 List<MovieVariant> Movies { 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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MovieVariant
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string File { get; set; }
|
||||||
|
public string Quality { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
41
UaTUT/OnlineApi.cs
Normal file
41
UaTUT/OnlineApi.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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 UaTUT
|
||||||
|
{
|
||||||
|
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.UaTUT;
|
||||||
|
// UaTUT: змішаний контент (аніме + не-аніме) — завжди включати при enable && !rip
|
||||||
|
if (init.enable && !init.rip)
|
||||||
|
{
|
||||||
|
string url = init.overridehost;
|
||||||
|
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||||
|
url = $"{host}/uatut";
|
||||||
|
|
||||||
|
online.Add((init.displayname, url, "uatut", init.displayindex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return online;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
UaTUT/UaTUT.csproj
Normal file
15
UaTUT/UaTUT.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>
|
||||||
478
UaTUT/UaTUTInvoke.cs
Normal file
478
UaTUT/UaTUTInvoke.cs
Normal file
@ -0,0 +1,478 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Shared.Engine;
|
||||||
|
using Shared.Models.Online.Settings;
|
||||||
|
using Shared.Models;
|
||||||
|
using UaTUT.Models;
|
||||||
|
|
||||||
|
namespace UaTUT
|
||||||
|
{
|
||||||
|
public class UaTUTInvoke
|
||||||
|
{
|
||||||
|
private static readonly Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase);
|
||||||
|
private static readonly Regex QualityFhdRegex = new Regex(@"(^|[^0-9])(1080p?)([^0-9]|$)|\bfhd\b", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
private OnlinesSettings _init;
|
||||||
|
private IHybridCache _hybridCache;
|
||||||
|
private Action<string> _onLog;
|
||||||
|
private ProxyManager _proxyManager;
|
||||||
|
|
||||||
|
public UaTUTInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||||
|
{
|
||||||
|
_init = init;
|
||||||
|
_hybridCache = hybridCache;
|
||||||
|
_onLog = onLog;
|
||||||
|
_proxyManager = proxyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SearchResult>> Search(string query, string imdbId = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string searchUrl = $"{_init.apihost}/search.php";
|
||||||
|
|
||||||
|
// Поступовий пошук: спочатку по imdbId, потім по назві
|
||||||
|
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($"UaTUT Search error: {ex.Message}");
|
||||||
|
return new List<SearchResult>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<SearchResult>> PerformSearch(string searchUrl, string query)
|
||||||
|
{
|
||||||
|
string url = $"{searchUrl}?q={HttpUtility.UrlEncode(query)}";
|
||||||
|
_onLog($"UaTUT searching: {url}");
|
||||||
|
|
||||||
|
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") };
|
||||||
|
var response = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(response))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var results = JsonConvert.DeserializeObject<List<SearchResult>>(response);
|
||||||
|
_onLog($"UaTUT found {results?.Count ?? 0} results for query: {query}");
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"UaTUT parse error: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetMoviePageContent(string movieId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string url = $"{_init.apihost}/{movieId}";
|
||||||
|
_onLog($"UaTUT getting movie page: {url}");
|
||||||
|
|
||||||
|
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") };
|
||||||
|
var response = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"UaTUT GetMoviePageContent error: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetPlayerUrl(string moviePageContent)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Шукаємо iframe з id="vip-player" та class="tab-content"
|
||||||
|
var match = Regex.Match(moviePageContent, @"<iframe[^>]*id=[""']vip-player[""'][^>]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
string playerUrl = match.Groups[1].Value;
|
||||||
|
_onLog($"UaTUT found player URL: {playerUrl}");
|
||||||
|
return playerUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onLog("UaTUT: vip-player iframe not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"UaTUT GetPlayerUrl error: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PlayerData> GetPlayerData(string playerUrl)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string sourceUrl = WithAshdiMultivoice(playerUrl);
|
||||||
|
string requestUrl = sourceUrl;
|
||||||
|
if (ApnHelper.IsAshdiUrl(sourceUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost))
|
||||||
|
requestUrl = ApnHelper.WrapUrl(_init, sourceUrl);
|
||||||
|
|
||||||
|
_onLog($"UaTUT getting player data from: {requestUrl}");
|
||||||
|
|
||||||
|
var headers = new List<HeadersModel>()
|
||||||
|
{
|
||||||
|
new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"),
|
||||||
|
new HeadersModel("Referer", sourceUrl.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase) ? "https://ashdi.vip/" : _init.apihost)
|
||||||
|
};
|
||||||
|
var response = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(response))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return ParsePlayerData(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"UaTUT GetPlayerData error: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayerData ParsePlayerData(string playerHtml)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var playerData = new PlayerData();
|
||||||
|
|
||||||
|
// Для фільмів шукаємо прямий file
|
||||||
|
var fileMatch = Regex.Match(playerHtml, @"file:'([^']+)'", RegexOptions.IgnoreCase);
|
||||||
|
if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("["))
|
||||||
|
{
|
||||||
|
playerData.File = fileMatch.Groups[1].Value;
|
||||||
|
playerData.Movies = new List<MovieVariant>()
|
||||||
|
{
|
||||||
|
new MovieVariant
|
||||||
|
{
|
||||||
|
File = playerData.File,
|
||||||
|
Quality = DetectQualityTag(playerData.File) ?? "auto",
|
||||||
|
Title = BuildMovieTitle("Основне джерело", playerData.File, 1)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_onLog($"UaTUT found direct file: {playerData.File}");
|
||||||
|
|
||||||
|
// Шукаємо poster
|
||||||
|
var posterMatch = Regex.Match(playerHtml, @"poster:[""']([^""']+)[""']", RegexOptions.IgnoreCase);
|
||||||
|
if (posterMatch.Success)
|
||||||
|
playerData.Poster = posterMatch.Groups[1].Value;
|
||||||
|
|
||||||
|
return playerData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для серіалів шукаємо JSON структуру з сезонами та озвучками
|
||||||
|
string jsonData = ExtractPlayerFileArray(playerHtml);
|
||||||
|
if (!string.IsNullOrWhiteSpace(jsonData))
|
||||||
|
{
|
||||||
|
string normalizedJson = WebUtility.HtmlDecode(jsonData)
|
||||||
|
.Replace("\\/", "/")
|
||||||
|
.Replace("\\'", "'")
|
||||||
|
.Replace("\\\"", "\"");
|
||||||
|
|
||||||
|
_onLog($"UaTUT found JSON data for series");
|
||||||
|
|
||||||
|
playerData.Movies = ParseMovieVariantsJson(normalizedJson);
|
||||||
|
playerData.File = playerData.Movies?.FirstOrDefault()?.File;
|
||||||
|
playerData.Voices = ParseVoicesJson(normalizedJson);
|
||||||
|
return playerData;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onLog("UaTUT: No player data found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"UaTUT ParsePlayerData error: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Voice> ParseVoicesJson(string jsonData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Декодуємо JSON структуру озвучок
|
||||||
|
dynamic voicesData = JsonConvert.DeserializeObject(jsonData);
|
||||||
|
var voices = new List<Voice>();
|
||||||
|
|
||||||
|
if (voicesData != null)
|
||||||
|
{
|
||||||
|
foreach (var voiceGroup in voicesData)
|
||||||
|
{
|
||||||
|
var voice = new Voice
|
||||||
|
{
|
||||||
|
Name = voiceGroup.title?.ToString(),
|
||||||
|
Seasons = new List<Season>()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (voiceGroup.folder != null)
|
||||||
|
{
|
||||||
|
foreach (var seasonData in voiceGroup.folder)
|
||||||
|
{
|
||||||
|
var season = new Season
|
||||||
|
{
|
||||||
|
Title = seasonData.title?.ToString(),
|
||||||
|
Episodes = new List<Episode>()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (seasonData.folder != null)
|
||||||
|
{
|
||||||
|
foreach (var episodeData in seasonData.folder)
|
||||||
|
{
|
||||||
|
var episode = new Episode
|
||||||
|
{
|
||||||
|
Title = episodeData.title?.ToString(),
|
||||||
|
File = episodeData.file?.ToString(),
|
||||||
|
Id = episodeData.id?.ToString(),
|
||||||
|
Poster = episodeData.poster?.ToString(),
|
||||||
|
Subtitle = episodeData.subtitle?.ToString()
|
||||||
|
};
|
||||||
|
season.Episodes.Add(episode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
voice.Seasons.Add(season);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
voices.Add(voice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onLog($"UaTUT parsed {voices.Count} voices");
|
||||||
|
return voices;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"UaTUT ParseVoicesJson error: {ex.Message}");
|
||||||
|
return new List<Voice>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MovieVariant> ParseMovieVariantsJson(string jsonData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = JsonConvert.DeserializeObject<List<dynamic>>(jsonData);
|
||||||
|
var movies = new List<MovieVariant>();
|
||||||
|
if (data == null || data.Count == 0)
|
||||||
|
return movies;
|
||||||
|
|
||||||
|
int index = 1;
|
||||||
|
foreach (var item in data)
|
||||||
|
{
|
||||||
|
string file = item?.file?.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(file))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string rawTitle = item?.title?.ToString();
|
||||||
|
movies.Add(new MovieVariant
|
||||||
|
{
|
||||||
|
File = file,
|
||||||
|
Quality = DetectQualityTag($"{rawTitle} {file}") ?? "auto",
|
||||||
|
Title = BuildMovieTitle(rawTitle, file, index)
|
||||||
|
});
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return movies;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog($"UaTUT ParseMovieVariantsJson error: {ex.Message}");
|
||||||
|
return new List<MovieVariant>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string WithAshdiMultivoice(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
return url;
|
||||||
|
|
||||||
|
if (url.IndexOf("ashdi.vip/vod/", StringComparison.OrdinalIgnoreCase) < 0)
|
||||||
|
return url;
|
||||||
|
|
||||||
|
if (url.IndexOf("multivoice", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
return url;
|
||||||
|
|
||||||
|
return url.Contains("?") ? $"{url}&multivoice" : $"{url}?multivoice";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildMovieTitle(string rawTitle, string file, int index)
|
||||||
|
{
|
||||||
|
string title = string.IsNullOrWhiteSpace(rawTitle) ? $"Варіант {index}" : StripMoviePrefix(WebUtility.HtmlDecode(rawTitle).Trim());
|
||||||
|
string qualityTag = DetectQualityTag($"{title} {file}");
|
||||||
|
if (string.IsNullOrWhiteSpace(qualityTag))
|
||||||
|
return title;
|
||||||
|
|
||||||
|
if (title.StartsWith("[4K]", StringComparison.OrdinalIgnoreCase) || title.StartsWith("[FHD]", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return title;
|
||||||
|
|
||||||
|
return $"{qualityTag} {title}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DetectQualityTag(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (Quality4kRegex.IsMatch(value))
|
||||||
|
return "[4K]";
|
||||||
|
|
||||||
|
if (QualityFhdRegex.IsMatch(value))
|
||||||
|
return "[FHD]";
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StripMoviePrefix(string title)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
|
return title;
|
||||||
|
|
||||||
|
string normalized = Regex.Replace(title, @"\s+", " ").Trim();
|
||||||
|
int sepIndex = normalized.LastIndexOf(" - ", StringComparison.Ordinal);
|
||||||
|
if (sepIndex <= 0 || sepIndex >= normalized.Length - 3)
|
||||||
|
return normalized;
|
||||||
|
|
||||||
|
string prefix = normalized.Substring(0, sepIndex).Trim();
|
||||||
|
string suffix = normalized.Substring(sepIndex + 3).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(suffix))
|
||||||
|
return normalized;
|
||||||
|
|
||||||
|
if (Regex.IsMatch(prefix, @"(19|20)\d{2}"))
|
||||||
|
return suffix;
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractPlayerFileArray(string html)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(html))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int searchIndex = 0;
|
||||||
|
while (searchIndex >= 0 && searchIndex < html.Length)
|
||||||
|
{
|
||||||
|
int fileIndex = html.IndexOf("file", searchIndex, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (fileIndex < 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int colonIndex = html.IndexOf(':', fileIndex);
|
||||||
|
if (colonIndex < 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int startIndex = colonIndex + 1;
|
||||||
|
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
|
||||||
|
startIndex++;
|
||||||
|
|
||||||
|
if (startIndex < html.Length && (html[startIndex] == '\'' || html[startIndex] == '"'))
|
||||||
|
{
|
||||||
|
startIndex++;
|
||||||
|
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
|
||||||
|
startIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIndex >= html.Length || html[startIndex] != '[')
|
||||||
|
{
|
||||||
|
searchIndex = fileIndex + 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExtractBracketArray(html, startIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractBracketArray(string text, int startIndex)
|
||||||
|
{
|
||||||
|
if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[')
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int depth = 0;
|
||||||
|
bool inString = false;
|
||||||
|
bool escaped = false;
|
||||||
|
char quoteChar = '\0';
|
||||||
|
|
||||||
|
for (int i = startIndex; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
char ch = text[i];
|
||||||
|
|
||||||
|
if (inString)
|
||||||
|
{
|
||||||
|
if (escaped)
|
||||||
|
{
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '\\')
|
||||||
|
{
|
||||||
|
escaped = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
UaTUT/manifest.json
Normal file
6
UaTUT/manifest.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"enable": true,
|
||||||
|
"version": 3,
|
||||||
|
"initspace": "UaTUT.ModInit",
|
||||||
|
"online": "UaTUT.OnlineApi"
|
||||||
|
}
|
||||||
1
check.sh
1
check.sh
@ -14,6 +14,7 @@ fi
|
|||||||
# Формат: SourceName|PageURL|ProbeURL
|
# Формат: SourceName|PageURL|ProbeURL
|
||||||
SOURCES=$(cat <<'SRC'
|
SOURCES=$(cat <<'SRC'
|
||||||
Uaflix|https://uafix.net/films/nozhi-nagolo-3/|
|
Uaflix|https://uafix.net/films/nozhi-nagolo-3/|
|
||||||
|
UaTUT|https://uk.uatut.fun/watch/2851|
|
||||||
AnimeON|https://animeon.club/anime/924-provodzhalnicya-friren|https://animeon.club/api/player/47960/episode
|
AnimeON|https://animeon.club/anime/924-provodzhalnicya-friren|https://animeon.club/api/player/47960/episode
|
||||||
Bamboo|https://bambooua.com/dorama/938-18_again.html|
|
Bamboo|https://bambooua.com/dorama/938-18_again.html|
|
||||||
Mikai|https://mikai.me/anime/1272-friren-shcho-provodzhaie-v-ostanniu-put|https://api.mikai.me/v1/anime/1272
|
Mikai|https://mikai.me/anime/1272-friren-shcho-provodzhaie-v-ostanniu-put|https://api.mikai.me/v1/anime/1272
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user