Compare commits

..

No commits in common. "3c475352fe8a8e7691ad1b96d6eec52fb0646d29" and "adfa97e810d98cb65fb8275b0c78062063e1e245" have entirely different histories.

14 changed files with 0 additions and 1966 deletions

View File

@ -1,306 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using LME.StreamData.Models;
using Microsoft.AspNetCore.Mvc;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.Settings;
using Shared.Models.Templates;
namespace LME.StreamData.Controllers
{
public class Controller : BaseOnlineController
{
ProxyManager proxyManager;
public Controller() : base(ModInit.Settings)
{
proxyManager = new ProxyManager(ModInit.StreamDataSettings);
}
/// <summary>
/// Головний ендпоінт модуля StreamData
/// Працює виключно через TMDB ID (параметр id)
/// </summary>
[HttpGet]
[Route("lite/lme_streamdata")]
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, int e = -1, bool play = false, bool rjson = false, string href = null, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = loadKit(ModInit.StreamDataSettings);
if (!init.enable)
return Forbid();
var invoke = new StreamDataInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
// checksearch — перевірка доступності
if (checksearch)
{
if (!IsCheckOnlineSearchEnabled())
return OnError("lme_streamdata", refresh_proxy: true);
if (id > 0)
return Content("data-json=", "text/plain; charset=utf-8");
return OnError("lme_streamdata", refresh_proxy: true);
}
// play — повернути стрім для конкретного епізоду (call метод)
if (play)
{
return await HandlePlay(invoke, init, id, s, e, title, original_title, t);
}
// Фільм
if (serial != 1)
{
return await HandleMovie(invoke, init, id, title, original_title, rjson);
}
// Серіал
return await HandleSerial(invoke, init, id, title, original_title, s, e, t, rjson);
}
/// <summary>
/// Обробка фільму: отримуємо всі stream_urls та показуємо їх
/// </summary>
private async Task<ActionResult> HandleMovie(StreamDataInvoke invoke, OnlinesSettings init, long tmdbId, string title, string originalTitle, bool rjson)
{
var response = await invoke.GetMovie(tmdbId);
if (response?.data?.stream_urls == null || response.data.stream_urls.Count == 0)
return OnError("lme_streamdata", refresh_proxy: true);
var streamUrls = response.data.stream_urls;
var subs = CollectSubtitles(response);
var displayTitle = !string.IsNullOrEmpty(title) ? title : (!string.IsNullOrEmpty(originalTitle) ? originalTitle : response.data.title);
var tpl = new MovieTpl(displayTitle, originalTitle);
int index = 1;
foreach (var streamUrl in streamUrls)
{
if (string.IsNullOrWhiteSpace(streamUrl))
continue;
string label = $"Джерело #{index}";
string processedUrl = BuildStreamUrl(init, streamUrl);
tpl.Append(label, processedUrl, subtitles: subs);
index++;
}
if (tpl.data == null || tpl.data.Count == 0)
return OnError("lme_streamdata", refresh_proxy: true);
return Content(
rjson ? tpl.ToJson() : tpl.ToHtml(),
rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8"
);
}
/// <summary>
/// Обробка серіалу: eps → сезони → епізоди з voice-вкладками (джерела)
/// </summary>
private async Task<ActionResult> HandleSerial(StreamDataInvoke invoke, OnlinesSettings init, long tmdbId, string title, string originalTitle, int s, int e, string t, bool rjson)
{
var response = await invoke.GetTvSeries(tmdbId);
if (response?.data?.eps == null || response.data.eps.Count == 0)
return OnError("lme_streamdata", refresh_proxy: true);
var eps = response.data.eps;
var seasons = eps.Keys
.Select(k => int.TryParse(k, out int sn) ? sn : 0)
.Where(sn => sn > 0)
.OrderBy(sn => sn)
.ToList();
if (seasons.Count == 0)
return OnError("lme_streamdata", refresh_proxy: true);
// Кількість джерел (CDN) з першого запиту
var sources = response.data?.stream_urls?.Where(u => !string.IsNullOrWhiteSpace(u)).ToList() ?? new List<string>();
int sourceCount = Math.Max(1, sources.Count);
var displayTitle = !string.IsNullOrEmpty(title) ? title : (!string.IsNullOrEmpty(originalTitle) ? originalTitle : response.data.title);
// Список сезонів
if (s <= 0)
{
var seasonTpl = new SeasonTpl(seasons.Count);
foreach (var season in seasons)
{
string seasonLink = $"{host}/lite/lme_streamdata?id={tmdbId}&serial=1&s={season}&title={HttpUtility.UrlEncode(displayTitle)}&original_title={HttpUtility.UrlEncode(originalTitle)}";
seasonTpl.Append($"Сезон {season}", seasonLink, season.ToString());
}
return Content(
rjson ? seasonTpl.ToJson() : seasonTpl.ToHtml(),
rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8"
);
}
// Список епізодів з voice-вкладками для вибору джерела
string seasonKey = s.ToString();
if (!eps.ContainsKey(seasonKey) || eps[seasonKey] == null || eps[seasonKey].Count == 0)
return OnError("lme_streamdata", refresh_proxy: true);
var episodeNumbers = eps[seasonKey]
.Select(ep => int.TryParse(ep, out int en) ? en : 0)
.Where(en => en > 0)
.OrderBy(en => en)
.ToList();
if (episodeNumbers.Count == 0)
return OnError("lme_streamdata", refresh_proxy: true);
// Voice-вкладки: кожне джерело як окрема "озвучка"
string selectedSource = string.IsNullOrEmpty(t) ? "1" : t;
int selectedIndex = int.TryParse(selectedSource, out int si) && si >= 1 && si <= sourceCount ? si : 1;
var voiceTpl = new VoiceTpl();
for (int i = 1; i <= sourceCount; i++)
{
string voiceLink = $"{host}/lite/lme_streamdata?id={tmdbId}&serial=1&s={s}&t={i}&title={HttpUtility.UrlEncode(displayTitle)}&original_title={HttpUtility.UrlEncode(originalTitle)}";
voiceTpl.Append($"Джерело #{i}", i == selectedIndex, voiceLink);
}
// Епізоди з посиланнями на вибране джерело
var episodeTpl = new EpisodeTpl(episodeNumbers.Count);
foreach (var epNum in episodeNumbers)
{
string episodeName = $"Епізод {epNum}";
string callUrl = $"{host}/lite/lme_streamdata?id={tmdbId}&serial=1&s={s}&e={epNum}&play=true&t={selectedSource}&title={HttpUtility.UrlEncode(displayTitle)}&original_title={HttpUtility.UrlEncode(originalTitle)}";
episodeTpl.Append(episodeName, displayTitle, s.ToString(), epNum.ToString("D2"), accsArgs(callUrl), "call");
}
episodeTpl.Append(voiceTpl);
return Content(
rjson ? episodeTpl.ToJson() : episodeTpl.ToHtml(),
rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8"
);
}
/// <summary>
/// Обробка play-запиту: API з season/episode → JSON з вибраним джерелом
/// </summary>
private async Task<ActionResult> HandlePlay(StreamDataInvoke invoke, OnlinesSettings init, long tmdbId, int season, int episode, string title, string originalTitle, string t)
{
if (tmdbId <= 0 || season <= 0 || episode <= 0)
return OnError("lme_streamdata", refresh_proxy: true);
var response = await invoke.GetEpisode(tmdbId, season, episode);
if (response?.data?.stream_urls == null || response.data.stream_urls.Count == 0)
return OnError("lme_streamdata", refresh_proxy: true);
var streamUrls = response.data.stream_urls.Where(u => !string.IsNullOrWhiteSpace(u)).ToList();
if (streamUrls.Count == 0)
return OnError("lme_streamdata", refresh_proxy: true);
// Вибираємо джерело за індексом з voice-вкладки t (1-based)
int sourceIndex = int.TryParse(t, out int si) && si >= 1 && si <= streamUrls.Count ? si - 1 : 0;
string streamUrl = BuildStreamUrl(init, streamUrls[sourceIndex]);
var subs = CollectSubtitles(response);
string displayTitle = !string.IsNullOrEmpty(title) ? title : (!string.IsNullOrEmpty(originalTitle) ? originalTitle : response.data.title);
return UpdateService.Validate(Content(
VideoTpl.ToJson("play", streamUrl, displayTitle, subtitles: subs),
"application/json; charset=utf-8"
));
}
/// <summary>
/// Зібрати субтитри з відповіді API
/// </summary>
private SubtitleTpl CollectSubtitles(StreamDataResponse response)
{
if (response?.default_subs == null || response.default_subs.Count == 0)
return null;
var tpl = new SubtitleTpl(response.default_subs.Count);
foreach (var sub in response.default_subs)
{
if (!string.IsNullOrWhiteSpace(sub?.url) && !string.IsNullOrWhiteSpace(sub?.lang))
{
tpl.Append(sub.lang, sub.url);
}
}
return tpl;
}
string BuildStreamUrl(OnlinesSettings init, string streamLink)
{
string link = StripLampacArgs(streamLink?.Trim());
if (string.IsNullOrEmpty(link))
return link;
if (ApnHelper.IsEnabled(init))
{
if (ModInit.ApnHostProvided)
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 static string StripLampacArgs(string url)
{
if (string.IsNullOrEmpty(url))
return url;
string cleaned = System.Text.RegularExpressions.Regex.Replace(
url,
@"([?&])(account_email|uid|nws_id)=[^&]*",
"$1",
System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned;
}
private static bool IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

View File

@ -1,91 +0,0 @@
using Newtonsoft.Json.Linq;
using Shared;
using Shared.Engine;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Shared.Models;
using Shared.Models.Events;
using Shared.Models.Online.Settings;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace LME.StreamData
{
public class ModInit : IModuleLoaded
{
public static double Version => 1.5;
public static OnlinesSettings StreamDataSettings;
public static bool ApnHostProvided;
public static OnlinesSettings Settings
{
get => StreamDataSettings;
set => StreamDataSettings = value;
}
/// <summary>
/// Модуль завантажено
/// </summary>
public void Loaded(InitspaceModel initspace)
{
StreamDataSettings = new OnlinesSettings("LME.StreamData", "https://streamdata.vaplayer.ru", streamproxy: false, useproxy: false)
{
displayname = "StreamData",
displayindex = 0,
proxy = new Shared.Models.Base.ProxySettings()
{
useAuth = true,
username = "",
password = "",
list = new string[] { "socks5://ip:port" }
}
};
var defaults = JObject.FromObject(StreamDataSettings);
defaults["enabled"] = true;
var conf = ModuleInvoke.Init("LME.StreamData", defaults);
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
conf.Remove("apn");
conf.Remove("apn_host");
StreamDataSettings = conf.ToObject<OnlinesSettings>();
if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, StreamDataSettings, useDefaultHostWhenEmpty: true);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
if (hasApn && apnEnabled)
{
StreamDataSettings.streamproxy = false;
}
else if (StreamDataSettings.streamproxy)
{
StreamDataSettings.apnstream = false;
StreamDataSettings.apn = null;
}
// Реєструємо плагін без пошуку — працюємо тільки через TMDB ID
OnlineRegistry.RegisterWithSearch("lme_streamdata");
}
public void Dispose()
{
}
}
public static class UpdateService
{
private static readonly ModuleUpdateService _service = new(
() => ModInit.Settings?.plugin,
() => ModInit.Version);
public static Task ConnectAsync(string host, CancellationToken cancellationToken = default)
=> _service.ConnectAsync(host, cancellationToken);
public static bool IsDisconnected()
=> _service.IsDisconnected();
public static ActionResult Validate(ActionResult result)
=> _service.Validate(result);
}
}

View File

@ -1,31 +0,0 @@
using System.Collections.Generic;
namespace LME.StreamData.Models
{
public class SubtitleItem
{
public string lang { get; set; }
public string code { get; set; }
public string url { get; set; }
}
public class StreamDataResponse
{
public string status_code { get; set; }
public StreamDataInfo data { get; set; }
public List<SubtitleItem> default_subs { get; set; }
}
public class StreamDataInfo
{
public string title { get; set; }
public string imdb_id { get; set; }
public int season { get; set; }
public string episode { get; set; }
public Dictionary<string, List<string>> eps { get; set; }
public string file_name { get; set; }
public string backdrop { get; set; }
public List<string> stream_urls { get; set; }
public string thumbnails_url { get; set; }
}
}

View File

@ -1,28 +0,0 @@
using Microsoft.AspNetCore.Http;
using Shared.Models;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
namespace LME.StreamData
{
public class OnlineApi : IModuleOnline
{
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
{
var online = new List<ModuleOnlineItem>();
var init = ModInit.StreamDataSettings;
if (init.enable && !init.rip)
{
if (UpdateService.IsDisconnected())
init.overridehost = null;
// StreamData працює з TMDB ID — показуємо для всього контенту
online.Add(new ModuleOnlineItem(init, "lme_streamdata"));
}
return online;
}
}
}

View File

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

View File

@ -1,155 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LME.StreamData.Models;
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.Settings;
namespace LME.StreamData
{
public class StreamDataInvoke
{
private readonly OnlinesSettings _init;
private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
private readonly HttpHydra _httpHydra;
private const string API_BASE = "https://streamdata.vaplayer.ru/api.php";
public StreamDataInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_httpHydra = httpHydra;
}
/// <summary>
/// Отримати дані для фільму за TMDB ID
/// </summary>
public async Task<StreamDataResponse> GetMovie(long tmdbId)
{
string memKey = $"StreamData:movie:{tmdbId}";
if (_hybridCache.TryGetValue(memKey, out StreamDataResponse cached))
return cached;
try
{
string url = $"{API_BASE}?tmdb={tmdbId}&type=movie";
_onLog?.Invoke($"StreamData movie: {url}");
string json = await ApiGet(url);
if (string.IsNullOrEmpty(json))
return null;
var response = JsonConvert.DeserializeObject<StreamDataResponse>(json);
if (response?.status_code != "200" || response?.data?.stream_urls == null || response.data.stream_urls.Count == 0)
return null;
_hybridCache.Set(memKey, response, cacheTime(30, init: _init));
return response;
}
catch (Exception ex)
{
_onLog?.Invoke($"StreamData movie error: {ex.Message}");
return null;
}
}
/// <summary>
/// Отримати дані для серіалу (без season/episode - отримуємо eps структуру + S01E01)
/// </summary>
public async Task<StreamDataResponse> GetTvSeries(long tmdbId)
{
string memKey = $"StreamData:tv:{tmdbId}";
if (_hybridCache.TryGetValue(memKey, out StreamDataResponse cached))
return cached;
try
{
string url = $"{API_BASE}?tmdb={tmdbId}&type=tv";
_onLog?.Invoke($"StreamData tv: {url}");
string json = await ApiGet(url);
if (string.IsNullOrEmpty(json))
return null;
var response = JsonConvert.DeserializeObject<StreamDataResponse>(json);
if (response?.status_code != "200" || response?.data?.eps == null || response.data.eps.Count == 0)
return null;
_hybridCache.Set(memKey, response, cacheTime(30, init: _init));
return response;
}
catch (Exception ex)
{
_onLog?.Invoke($"StreamData tv error: {ex.Message}");
return null;
}
}
/// <summary>
/// Отримати стріми для конкретного епізоду
/// </summary>
public async Task<StreamDataResponse> GetEpisode(long tmdbId, int season, int episode)
{
string memKey = $"StreamData:ep:{tmdbId}:s{season}e{episode}";
if (_hybridCache.TryGetValue(memKey, out StreamDataResponse cached))
return cached;
try
{
string url = $"{API_BASE}?tmdb={tmdbId}&type=tv&season={season}&episode={episode}";
_onLog?.Invoke($"StreamData episode: {url}");
string json = await ApiGet(url);
if (string.IsNullOrEmpty(json))
return null;
var response = JsonConvert.DeserializeObject<StreamDataResponse>(json);
if (response?.status_code != "200" || response?.data?.stream_urls == null || response.data.stream_urls.Count == 0)
return null;
_hybridCache.Set(memKey, response, cacheTime(30, init: _init));
return response;
}
catch (Exception ex)
{
_onLog?.Invoke($"StreamData episode error: {ex.Message}");
return null;
}
}
private Task<string> ApiGet(string url)
{
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://brightpathsignals.com/"),
new HeadersModel("X-Requested-With", "XMLHttpRequest")
};
if (_httpHydra != null)
return _httpHydra.Get(url, newheaders: headers);
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
}
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
{
if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub);
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;
return TimeSpan.FromMinutes(ctime);
}
}
}

View File

@ -1,12 +0,0 @@
{
"enable": true,
"version": 3,
"initspace": "LME.StreamData.ModInit",
"online": "LME.StreamData.OnlineApi",
"syntaxPaths": [
"../LME.Shared/GlobalUsings.cs",
"../LME.Shared/Online/OnlineRegistry.cs",
"../LME.Shared/Update/ModuleUpdateService.cs",
"../LME.Shared/Apn/ApnHelper.cs"
]
}

View File

@ -1,316 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using LME.UAKino.Models;
using Microsoft.AspNetCore.Mvc;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.Settings;
using Shared.Models.Templates;
namespace LME.UAKino.Controllers
{
public class Controller : BaseOnlineController
{
ProxyManager proxyManager;
public Controller() : base(ModInit.Settings)
{
proxyManager = new ProxyManager(ModInit.UAKino);
}
[HttpGet]
[Route("lite/lme_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, int s = -1, bool rjson = false, string href = null, bool checksearch = false)
{
await UpdateService.ConnectAsync(host);
var init = loadKit(ModInit.UAKino);
if (!init.enable)
return Forbid();
var invoke = new UAKinoInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
if (checksearch)
{
if (!IsCheckOnlineSearchEnabled())
return OnError("lme_uakino", refresh_proxy: true);
var searchResults = await invoke.Search(title, original_title, year, imdb_id);
if (searchResults != null && searchResults.Count > 0)
return Content("data-json=", "text/plain; charset=utf-8");
return OnError("lme_uakino", refresh_proxy: true);
}
string newsId = null;
string itemUrl = href;
if (string.IsNullOrEmpty(itemUrl))
{
// === ПЕРШИЙ ЗАПИТ: пошук ===
var searchResults = await invoke.Search(title, original_title, year, imdb_id);
if (searchResults == null || searchResults.Count == 0)
return OnError("lme_uakino", refresh_proxy: true);
if (serial == 1)
{
// Серіал
if (searchResults.Count == 1)
{
var sr = searchResults[0];
if (sr.Seasons.Count > 1 && s == -1)
{
// Кілька сезонів — показуємо SeasonTpl для вибору
return HandleSeasonSelection(sr, id, imdb_id, kinopoisk_id, title, original_title, year, rjson);
}
// Один сезон — використовуємо його
itemUrl = sr.Seasons[0].Url;
newsId = sr.Seasons[0].NewsId;
}
else
{
// Кілька різних шоу — обирає
return ShowSimilarTpl(searchResults, id, imdb_id, kinopoisk_id, title, original_title, year, serial, rjson);
}
}
else
{
// Фільм
if (searchResults.Count > 1)
{
return ShowSimilarTpl(searchResults, id, imdb_id, kinopoisk_id, title, original_title, year, serial, rjson);
}
itemUrl = searchResults[0].Seasons[0].Url;
newsId = searchResults[0].Seasons[0].NewsId;
}
}
else
{
// Повторний запит (з селектора сезонів або озвучок)
newsId = UAKinoInvoke.ExtractNewsId(itemUrl);
}
if (string.IsNullOrEmpty(newsId))
return OnError("lme_uakino", refresh_proxy: true);
var voices = await invoke.GetPlaylist(newsId);
if (voices == null || voices.Count == 0)
{
// Fallback: playlist API повернув ERR_NOT_DATA — пробуємо зі сторінки
string fallbackUrl = await invoke.GetPageFallbackUrl(itemUrl);
if (!string.IsNullOrEmpty(fallbackUrl))
{
if (serial == 1)
{
var voice_tpl = new VoiceTpl();
var episode_tpl = new EpisodeTpl();
string resolvedUrl = await invoke.ResolveAshdiVod(fallbackUrl);
string streamUrl = BuildStreamUrl(init, resolvedUrl);
voice_tpl.Append("Озвучення", true, null);
episode_tpl.Append("Епізод 1", title ?? original_title, s >= 0 ? s.ToString() : "1", "01", streamUrl);
episode_tpl.Append(voice_tpl);
return rjson
? Content(episode_tpl.ToJson(), "application/json; charset=utf-8")
: Content(episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
else
{
var resolvedStreams = await invoke.ResolveAshdiVodAll(fallbackUrl);
var movie_tpl = new MovieTpl(title, original_title, resolvedStreams.Count);
foreach (var (file, label) in resolvedStreams)
{
string displayLabel = !string.IsNullOrEmpty(label) ? label : "Фільм";
string streamUrl = BuildStreamUrl(init, file);
movie_tpl.Append(displayLabel, streamUrl);
}
return rjson
? Content(movie_tpl.ToJson(), "application/json; charset=utf-8")
: Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
}
}
return OnError("lme_uakino", refresh_proxy: true);
}
if (serial == 1)
{
return await HandleSerial(init, voices, title, original_title, imdb_id, kinopoisk_id, itemUrl, s, t, rjson, invoke);
}
else
{
return await HandleMovie(init, voices, title, original_title, rjson, invoke);
}
}
/// <summary>Вибір сезону для багатосезонного серіалу</summary>
private ActionResult HandleSeasonSelection(SearchResult sr, long id, string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool rjson)
{
var season_tpl = new SeasonTpl(sr.Seasons.Count);
foreach (var season in sr.Seasons)
{
string link = $"{host}/lite/lme_uakino?id={id}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season.SeasonNumber}&href={HttpUtility.UrlEncode(season.Url)}";
season_tpl.Append($"Сезон {season.SeasonNumber}", link, season.SeasonNumber.ToString());
}
return rjson
? Content(season_tpl.ToJson(), "application/json; charset=utf-8")
: Content(season_tpl.ToHtml(), "text/html; charset=utf-8");
}
/// <summary>Вибір між різними шоу/фільмами</summary>
private ActionResult ShowSimilarTpl(List<SearchResult> searchResults, long id, string imdb_id, long kinopoisk_id, string title, string original_title, int year, int serial, bool rjson)
{
var similar_tpl = new SimilarTpl(searchResults.Count);
foreach (var res in searchResults)
{
string seasonUrl = res.Seasons.Count > 0 ? res.Seasons[0].Url : "";
string yearStr = res.Seasons.Count > 0 ? (res.Seasons[0].Year?.ToString() ?? "") : (res.Year?.ToString() ?? "");
string link = $"{host}/lite/lme_uakino?id={id}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(seasonUrl)}";
similar_tpl.Append(res.Title, yearStr, res.OriginalTitle ?? "", link, res.Poster);
}
return rjson
? Content(similar_tpl.ToJson(), "application/json; charset=utf-8")
: Content(similar_tpl.ToHtml(), "text/html; charset=utf-8");
}
/// <summary>Серіал: озвучки + епізоди</summary>
private async Task<ActionResult> HandleSerial(OnlinesSettings init, List<VoiceGroup> voices, string title, string original_title, string imdb_id, long kinopoisk_id, string itemUrl, int s, string t, bool rjson, UAKinoInvoke invoke)
{
var voice_tpl = new VoiceTpl();
var episode_tpl = new EpisodeTpl();
if (string.IsNullOrEmpty(t))
t = voices.First().DataId;
foreach (var voice in voices)
{
string voiceLink = $"{host}/lite/lme_uakino?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&serial=1&s={s}&t={voice.DataId}&href={HttpUtility.UrlEncode(itemUrl)}";
voice_tpl.Append(voice.Name, voice.DataId == t, voiceLink);
}
var selected = voices.FirstOrDefault(v => v.DataId == t);
if (selected == null || selected.Episodes.Count == 0)
return OnError("lme_uakino", refresh_proxy: true);
string seasonStr = s >= 0 ? s.ToString() : "1";
foreach (var ep in selected.Episodes.OrderBy(e => e.EpisodeNumber ?? int.MaxValue))
{
int epNum = ep.EpisodeNumber ?? 1;
string epName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {epNum}" : ep.Title;
string fileUrl = await invoke.ResolveAshdiVod(ep.FileUrl);
string streamUrl = BuildStreamUrl(init, fileUrl);
episode_tpl.Append(epName, title ?? original_title, seasonStr, epNum.ToString("D2"), streamUrl);
}
episode_tpl.Append(voice_tpl);
return rjson
? Content(episode_tpl.ToJson(), "application/json; charset=utf-8")
: Content(episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
/// <summary>Фільм: список стрімів</summary>
private async Task<ActionResult> HandleMovie(OnlinesSettings init, List<VoiceGroup> voices, string title, string original_title, bool rjson, UAKinoInvoke invoke)
{
var processed = new HashSet<string>();
var movie_tpl = new MovieTpl(title, original_title);
foreach (var voice in voices)
{
foreach (var ep in voice.Episodes)
{
string label = voice.Name;
if (voices.Count == 1 && voice.Episodes.Count > 1)
label = ep.Title;
string fileUrl = ep.FileUrl;
// Резолвимо Ashdi VOD — отримуємо реальний .m3u8 стрім
string resolvedUrl = await invoke.ResolveAshdiVod(fileUrl);
// Дедуплікація: якщо той самий стрім — пропускаємо
if (!processed.Add(resolvedUrl))
continue;
string streamUrl = BuildStreamUrl(init, resolvedUrl);
movie_tpl.Append(label, streamUrl);
}
}
return rjson
? Content(movie_tpl.ToJson(), "application/json; charset=utf-8")
: Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
}
string BuildStreamUrl(OnlinesSettings init, string streamLink)
{
string link = StripLampacArgs(streamLink?.Trim());
if (string.IsNullOrEmpty(link))
return link;
if (ApnHelper.IsEnabled(init))
{
if (ModInit.ApnHostProvided || ApnHelper.IsAshdiUrl(link))
return ApnHelper.WrapUrl(init, link);
var noApn = (OnlinesSettings)init.Clone();
noApn.apnstream = false;
noApn.apn = null;
return HostStreamProxy(noApn, link);
}
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 IsCheckOnlineSearchEnabled()
{
try
{
var onlineType = Type.GetType("Online.ModInit");
if (onlineType == null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
onlineType = asm.GetType("Online.ModInit");
if (onlineType != null)
break;
}
}
var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var conf = confField?.GetValue(null);
var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (checkProp?.GetValue(conf) is bool enabled)
return enabled;
}
catch
{
}
return true;
}
private static void OnLog(string message)
{
System.Console.WriteLine(message);
}
}
}

View File

@ -1,99 +0,0 @@
using Newtonsoft.Json.Linq;
using Shared;
using Shared.Engine;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
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 LME.UAKino
{
public class ModInit : IModuleLoaded
{
public static double Version => 1.0;
public static OnlinesSettings UAKino;
public static bool ApnHostProvided;
public static OnlinesSettings Settings
{
get => UAKino;
set => UAKino = value;
}
/// <summary>
/// модуль загружен
/// </summary>
public void Loaded(InitspaceModel initspace)
{
UAKino = new OnlinesSettings("LME.UAKino", "https://uakino.top", streamproxy: false, useproxy: false)
{
displayname = "UAKino",
displayindex = 0,
proxy = new Shared.Models.Base.ProxySettings()
{
useAuth = true,
username = "",
password = "",
list = new string[] { "socks5://ip:port" }
}
};
var defaults = JObject.FromObject(UAKino);
defaults["enabled"] = true;
var conf = ModuleInvoke.Init("LME.UAKino", defaults);
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
conf.Remove("apn");
conf.Remove("apn_host");
UAKino = conf.ToObject<OnlinesSettings>();
if (hasApn)
ApnHelper.ApplyInitConf(apnEnabled, apnHost, UAKino, useDefaultHostWhenEmpty: true);
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
if (hasApn && apnEnabled)
{
UAKino.streamproxy = false;
}
else if (UAKino.streamproxy)
{
UAKino.apnstream = false;
UAKino.apn = null;
}
// Виводити "уточнити пошук"
OnlineRegistry.RegisterWithSearch("lme_uakino");
}
public void Dispose()
{
}
}
public static class UpdateService
{
private static readonly ModuleUpdateService _service = new(
() => ModInit.Settings?.plugin,
() => ModInit.Version);
public static Task ConnectAsync(string host, CancellationToken cancellationToken = default)
=> _service.ConnectAsync(host, cancellationToken);
public static bool IsDisconnected()
=> _service.IsDisconnected();
public static ActionResult Validate(ActionResult result)
=> _service.Validate(result);
}
}

View File

@ -1,37 +0,0 @@
using System.Collections.Generic;
namespace LME.UAKino.Models
{
public class SearchResult
{
public string Title { get; set; }
public string OriginalTitle { get; set; }
public string Poster { get; set; }
/// <summary>Сезони серіалу. Для фільмів — один елемент без SeasonNumber</summary>
public List<SeasonEntry> Seasons { get; set; } = new();
/// <summary>Рік фільму (тільки для не-сезонних результатів)</summary>
public int? Year { get; set; }
}
public class SeasonEntry
{
public int SeasonNumber { get; set; }
public string NewsId { get; set; }
public string Url { get; set; }
public int? Year { get; set; }
}
public class VoiceGroup
{
public string Name { get; set; }
public string DataId { get; set; }
public List<EpisodeItem> Episodes { get; set; } = new();
}
public class EpisodeItem
{
public string Title { get; set; }
public string FileUrl { get; set; }
public int? EpisodeNumber { get; set; }
}
}

View File

@ -1,35 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Shared.Models;
using Shared.Models.Module;
using Shared.Models.Module.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace LME.UAKino
{
public class OnlineApi : IModuleOnline
{
public List<ModuleOnlineItem> Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args)
{
long.TryParse(args.id, out long tmdbid);
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
}
private static List<ModuleOnlineItem> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
{
var online = new List<ModuleOnlineItem>();
var init = ModInit.UAKino;
if (init.enable && !init.rip)
{
if (UpdateService.IsDisconnected())
init.overridehost = null;
online.Add(new ModuleOnlineItem(init, "lme_uakino"));
}
return online;
}
}
}

View File

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

View File

@ -1,814 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using LME.UAKino.Models;
using HtmlAgilityPack;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.Settings;
namespace LME.UAKino
{
public class UAKinoInvoke
{
private readonly OnlinesSettings _init;
private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
private readonly HttpHydra _httpHydra;
public UAKinoInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager, HttpHydra httpHydra = null)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
_httpHydra = httpHydra;
}
public async Task<List<SearchResult>> Search(string title, string original_title, int year, string imdb_id)
{
string query = BuildSearchQuery(title, original_title, imdb_id);
if (string.IsNullOrEmpty(query))
return null;
string memKey = $"UAKino:search:{query}";
if (_hybridCache.TryGetValue(memKey, out List<SearchResult> cached))
return cached;
try
{
_onLog?.Invoke($"UAKino search: {query}");
string url = $"{_init.host}/engine/lazydev/dle_search/ajax.php";
string body = $"story={HttpUtility.UrlEncode(query)}&thisUrl=/ua/";
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.3 Safari/605.1.15"),
new HeadersModel("Referer", $"{_init.host}/ua/"),
new HeadersModel("X-Requested-With", "XMLHttpRequest"),
new HeadersModel("Origin", _init.host),
new HeadersModel("Accept", "*/*"),
new HeadersModel("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
};
string json = await Http.Post(_init.cors(url), body, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(json))
return null;
using var jsonDoc = JsonDocument.Parse(json);
if (!jsonDoc.RootElement.TryGetProperty("content", out JsonElement contentElem))
return null;
string html = contentElem.GetString();
if (string.IsNullOrEmpty(html))
return null;
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html);
var rawItems = ParseRawItems(htmlDoc);
var results = GroupByShow(rawItems);
if (results.Count > 0)
_hybridCache.Set(memKey, results, cacheTime(20));
return results;
}
catch (Exception ex)
{
_onLog?.Invoke($"UAKino search error: {ex.Message}");
return null;
}
}
/// <summary>
/// Отримати плейлист (озвучки + епізоди) за news_id
/// </summary>
public async Task<List<VoiceGroup>> GetPlaylist(string newsId)
{
if (string.IsNullOrEmpty(newsId))
return null;
string memKey = $"UAKino:playlist:{newsId}";
if (_hybridCache.TryGetValue(memKey, out List<VoiceGroup> cached))
return cached;
try
{
_onLog?.Invoke($"UAKino playlist: {newsId}");
string url = $"{_init.host}/engine/ajax/playlists.php?news_id={newsId}&xfield=playlist";
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.3 Safari/605.1.15"),
new HeadersModel("Referer", $"{_init.host}/{newsId}-"),
new HeadersModel("X-Requested-With", "XMLHttpRequest"),
new HeadersModel("Accept", "application/json, text/javascript, */*; q=0.01")
};
string json = await HttpGet(url, headers);
if (string.IsNullOrEmpty(json))
return null;
using var jsonDoc = JsonDocument.Parse(json);
if (!jsonDoc.RootElement.TryGetProperty("response", out JsonElement responseElem))
return null;
string html = responseElem.GetString();
if (string.IsNullOrEmpty(html))
return null;
var voices = ParsePlaylistHtml(html);
if (voices.Count > 0)
_hybridCache.Set(memKey, voices, cacheTime(30));
return voices;
}
catch (Exception ex)
{
_onLog?.Invoke($"UAKino playlist error: {ex.Message}");
return null;
}
}
/// <summary>
/// Fallback: отримати стрім з HTML сторінки фільму коли playlist API недоступний
/// Парсить &lt;link itemprop="video" value="..."&gt; або &lt;iframe id="pre" src="..."&gt;
/// </summary>
public async Task<string> GetPageFallbackUrl(string pageUrl)
{
if (string.IsNullOrEmpty(pageUrl))
return null;
try
{
_onLog?.Invoke($"UAKino page fallback: {pageUrl}");
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.3 Safari/605.1.15"),
new HeadersModel("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
new HeadersModel("Referer", _init.host)
};
string html = await HttpGet(pageUrl, headers);
if (string.IsNullOrEmpty(html))
return null;
var doc = new HtmlDocument();
doc.LoadHtml(html);
// Спершу пробуємо <link itemprop="video" value="...">
var linkTag = doc.DocumentNode.SelectSingleNode("//link[@itemprop='video']");
if (linkTag != null)
{
string value = linkTag.GetAttributeValue("value", "");
if (!string.IsNullOrEmpty(value))
return NormalizeUrl(value);
}
// Fallback до <iframe id="pre" src="...">
var iframeTag = doc.DocumentNode.SelectSingleNode("//iframe[@id='pre']");
if (iframeTag != null)
{
string src = iframeTag.GetAttributeValue("src", "");
if (!string.IsNullOrEmpty(src))
return NormalizeUrl(src);
}
return null;
}
catch (Exception ex)
{
_onLog?.Invoke($"UAKino page fallback error: {ex.Message}");
return null;
}
}
/// <summary>
/// Резолв Ashdi VOD сторінки: отримати реальний .m3u8 стрім з Playerjs file:'...'
/// </summary>
public async Task<string> ResolveAshdiVod(string vodUrl)
{
if (string.IsNullOrEmpty(vodUrl) || !ApnHelper.IsAshdiUrl(vodUrl))
return vodUrl;
try
{
string fetchUrl = vodUrl;
// Не додаємо ?multivoice — кожен VOD має свій унікальний стрім
// ?multivoice змішує всі голоси в один масив
_onLog?.Invoke($"UAKino resolve Ashdi: {fetchUrl}");
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", Http.UserAgent),
new HeadersModel("Referer", "https://ashdi.vip/")
};
if (ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost))
fetchUrl = ApnHelper.WrapUrl(_init, fetchUrl);
string html = await HttpGet(fetchUrl, headers);
if (string.IsNullOrEmpty(html))
return vodUrl;
// Спершу простий pattern file:'url'
var fileMatch = Regex.Match(html, @"file:\s*'([^']+)'", RegexOptions.IgnoreCase);
if (!fileMatch.Success)
fileMatch = Regex.Match(html, @"file:\s*""([^""]+)""", RegexOptions.IgnoreCase);
if (fileMatch.Success)
{
string resolvedUrl = fileMatch.Groups[1].Value;
if (!string.IsNullOrEmpty(resolvedUrl) && !resolvedUrl.StartsWith("["))
{
_onLog?.Invoke($"UAKino resolved Ashdi: {resolvedUrl}");
return resolvedUrl;
}
}
// Складний масив — знаходимо file:'[' і витягуємо збалансований JSON
int arrayStart = FindAshdiJsonArray(html);
if (arrayStart >= 0)
{
string jsonArray = ExtractBalancedBrackets(html, arrayStart);
if (!string.IsNullOrEmpty(jsonArray))
{
try
{
using var arr = JsonDocument.Parse(jsonArray);
if (arr.RootElement.ValueKind == JsonValueKind.Array && arr.RootElement.GetArrayLength() > 0)
{
string firstFile = arr.RootElement[0].GetProperty("file").GetString();
if (!string.IsNullOrEmpty(firstFile))
{
_onLog?.Invoke($"UAKino resolved Ashdi (array): {firstFile}");
return firstFile;
}
}
}
catch { }
}
}
return vodUrl;
}
catch (Exception ex)
{
_onLog?.Invoke($"UAKino resolve Ashdi error: {ex.Message}");
return vodUrl;
}
}
/// <summary>
/// Резолв Ashdi VOD з ?multivoice: повертає ВСІ стріми з JSON масиву
/// </summary>
public async Task<List<(string file, string title)>> ResolveAshdiVodAll(string vodUrl)
{
var result = new List<(string file, string title)>();
if (string.IsNullOrEmpty(vodUrl) || !ApnHelper.IsAshdiUrl(vodUrl))
{
if (!string.IsNullOrEmpty(vodUrl))
result.Add((vodUrl, null));
return result;
}
try
{
_onLog?.Invoke($"UAKino resolve Ashdi all: {vodUrl}");
// Для ?multivoice — Ашді повертає всі стріми в одному масиві
string fetchUrl = vodUrl;
if (!fetchUrl.Contains("multivoice"))
fetchUrl += (fetchUrl.Contains("?") ? "&" : "?") + "multivoice";
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", Http.UserAgent),
new HeadersModel("Referer", "https://ashdi.vip/")
};
if (ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost))
fetchUrl = ApnHelper.WrapUrl(_init, fetchUrl);
string html = await HttpGet(fetchUrl, headers);
if (string.IsNullOrEmpty(html))
{
result.Add((vodUrl, null));
return result;
}
int arrayStart = FindAshdiJsonArray(html);
if (arrayStart >= 0)
{
string jsonArray = ExtractBalancedBrackets(html, arrayStart);
if (!string.IsNullOrEmpty(jsonArray))
{
try
{
using var arr = JsonDocument.Parse(jsonArray);
if (arr.RootElement.ValueKind == JsonValueKind.Array)
{
foreach (var item in arr.RootElement.EnumerateArray())
{
string file = item.GetProperty("file").GetString();
string title = item.TryGetProperty("title", out var t) ? t.GetString() : null;
if (!string.IsNullOrEmpty(file))
result.Add((file, title?.Trim()));
}
}
}
catch { }
}
}
if (result.Count == 0)
result.Add((vodUrl, null));
return result;
}
catch (Exception ex)
{
_onLog?.Invoke($"UAKino resolve Ashdi all error: {ex.Message}");
result.Add((vodUrl, null));
return result;
}
}
/// <summary>
/// Знайти позицію JSON масиву `[{...}]` після `file:'`
/// </summary>
private static int FindAshdiJsonArray(string html)
{
int idx = html.IndexOf("file:'[", StringComparison.OrdinalIgnoreCase);
if (idx < 0)
idx = html.IndexOf("file:\"[", StringComparison.OrdinalIgnoreCase);
if (idx < 0)
return -1;
int bracket = html.IndexOf('[', idx);
return bracket;
}
/// <summary>
/// Витягнути збалансований вміст між [ ] з урахуванням вкладеності та рядків
/// </summary>
private static string ExtractBalancedBrackets(string text, int startIndex)
{
if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[')
return null;
int depth = 0;
bool inString = false;
char quote = '\0';
for (int i = startIndex; i < text.Length; i++)
{
char ch = text[i];
if (inString)
{
if (ch == '\\')
{
i++; // пропускаємо екранований символ
continue;
}
if (ch == quote)
inString = false;
continue;
}
if (ch == '"' || ch == '\'')
{
inString = true;
quote = ch;
continue;
}
if (ch == '[')
{
depth++;
continue;
}
if (ch == ']')
{
depth--;
if (depth == 0)
return text.Substring(startIndex, i - startIndex + 1);
}
}
return null;
}
/// <summary>
/// Витягнути news_id з URL контенту
/// </summary>
public static string ExtractNewsId(string url)
{
if (string.IsNullOrEmpty(url))
return null;
var match = Regex.Match(url, @"[?/](\d+)-[^/]*\.html");
if (match.Success)
return match.Groups[1].Value;
return null;
}
// ===================== Парсинг результатів пошуку =====================
/// <summary>Сирий елемент з HTML пошуку, до групування</summary>
private class RawSearchItem
{
public string Title { get; set; }
public string OriginalTitle { get; set; }
public string Url { get; set; }
public string Poster { get; set; }
public int? Year { get; set; }
public string NewsId { get; set; }
}
private List<RawSearchItem> ParseRawItems(HtmlDocument doc)
{
var items = new List<RawSearchItem>();
var nodes = doc.DocumentNode.SelectNodes("//a[@class='search-result-link']");
if (nodes == null)
return items;
foreach (var node in nodes)
{
try
{
string href = node.GetAttributeValue("href", "");
if (string.IsNullOrEmpty(href))
continue;
var imgNode = node.SelectSingleNode(".//img[@class='search-poster']");
string poster = imgNode?.GetAttributeValue("src", "") ?? "";
var titleNode = node.SelectSingleNode(".//span[@class='searchheading']");
string title = CleanText(titleNode?.InnerText);
var origTitleNode = node.SelectSingleNode(".//span[@class='search-orig-title']");
string origTitle = CleanText(origTitleNode?.InnerText);
var infoNode = node.SelectSingleNode(".//div[@class='search-extend-info']");
int? year = null;
if (infoNode != null)
{
var yearSpan = infoNode.SelectSingleNode("./span[1]");
string yearText = CleanText(yearSpan?.InnerText);
if (!string.IsNullOrEmpty(yearText) && int.TryParse(yearText.Trim(), out int parsedYear))
year = parsedYear;
}
// Фільтр: пропускаємо новини/трейлери — без року та без оригінальної назви
if (!IsRealContent(title, origTitle, year))
continue;
string newsId = ExtractNewsId(href);
items.Add(new RawSearchItem
{
Title = title,
OriginalTitle = origTitle,
Url = NormalizeUrl(href),
Poster = NormalizeUrl(poster),
Year = year,
NewsId = newsId
});
}
catch
{
continue;
}
}
return items;
}
/// <summary>Фільтр: реальний контент (не новина/трейлер)</summary>
private static bool IsRealContent(string title, string origTitle, int? year)
{
// Є рік — контент
if (year.HasValue)
return true;
// Є оригінальна назва — контент
if (!string.IsNullOrEmpty(origTitle))
return true;
// Дуже довга назва без року — скоріше новина
if (!string.IsNullOrEmpty(title) && title.Length > 50)
return false;
return false;
}
/// <summary>Групування сирих елементів по назві шоу. Кожна група = один SearchResult зі списком сезонів</summary>
private List<SearchResult> GroupByShow(List<RawSearchItem> rawItems)
{
if (rawItems.Count == 0)
return new List<SearchResult>();
var groups = new Dictionary<string, List<RawSearchItem>>();
foreach (var item in rawItems)
{
string cleanTitle = CleanShowTitle(item.Title);
string key = $"{cleanTitle.ToLowerInvariant()}|{(item.OriginalTitle ?? "").ToLowerInvariant()}";
if (!groups.ContainsKey(key))
groups[key] = new List<RawSearchItem>();
groups[key].Add(item);
}
var results = new List<SearchResult>();
foreach (var kvp in groups)
{
var items = kvp.Value;
var first = items[0];
string showTitle = CleanShowTitle(first.Title);
var sr = new SearchResult
{
Title = showTitle,
OriginalTitle = first.OriginalTitle,
Poster = first.Poster
};
foreach (var item in items)
{
int? seasonNum = ExtractSeasonNumber(item.Title);
if (seasonNum.HasValue)
{
sr.Seasons.Add(new SeasonEntry
{
SeasonNumber = seasonNum.Value,
NewsId = item.NewsId,
Url = item.Url,
Year = item.Year
});
}
else
{
// Фільм або контент без сезону
sr.Seasons.Add(new SeasonEntry
{
SeasonNumber = 1,
NewsId = item.NewsId,
Url = item.Url,
Year = item.Year
});
sr.Year = item.Year;
}
}
// Сортуємо сезони за номером
sr.Seasons = sr.Seasons.OrderBy(s => s.SeasonNumber).ToList();
results.Add(sr);
}
return results;
}
/// <summary>Витягти чисту назву шоу (без "N сезон" суфіксу)</summary>
private static string CleanShowTitle(string title)
{
if (string.IsNullOrEmpty(title))
return title;
return Regex.Replace(title, @"\s*\d+\s*сезон\s*$", "", RegexOptions.IgnoreCase).Trim();
}
/// <summary>Витягти номер сезону з назви</summary>
private static int? ExtractSeasonNumber(string title)
{
if (string.IsNullOrEmpty(title))
return null;
var match = Regex.Match(title, @"(\d+)\s*сезон", RegexOptions.IgnoreCase);
if (match.Success && int.TryParse(match.Groups[1].Value, out int num))
return num;
return null;
}
// ===================== Парсинг плейлиста =====================
private List<VoiceGroup> ParsePlaylistHtml(string html)
{
var voices = new List<VoiceGroup>();
var doc = new HtmlDocument();
doc.LoadHtml(html);
var playerDiv = doc.DocumentNode.SelectSingleNode("//div[@class='playlists-player']");
if (playerDiv == null)
{
return ParseEpisodesFlat(doc.DocumentNode);
}
// Парсимо голоси (озвучки) з вкладки playlists-lists
var voiceItems = playerDiv.SelectNodes(".//div[@class='playlists-lists']//ul/li");
bool hasVoiceTabs = voiceItems != null && voiceItems.Count > 0;
if (hasVoiceTabs)
{
foreach (var li in voiceItems)
{
string dataId = li.GetAttributeValue("data-id", "");
string text = CleanText(li.InnerText);
string voiceName = Regex.Replace(text, @"\s*\(\d+[\d,\s-]*\)\s*$", "").Trim();
if (string.IsNullOrEmpty(voiceName))
voiceName = text;
if (!string.IsNullOrEmpty(dataId))
{
voices.Add(new VoiceGroup
{
Name = voiceName,
DataId = dataId,
Episodes = new List<EpisodeItem>()
});
}
}
}
// Парсимо епізоди з playlists-videos
var episodeItems = playerDiv.SelectNodes(".//div[@class='playlists-videos']//ul/li[@data-file]");
if (episodeItems != null)
{
foreach (var li in episodeItems)
{
string fileUrl = li.GetAttributeValue("data-file", "");
string dataId = li.GetAttributeValue("data-id", "");
string voiceAttr = li.GetAttributeValue("data-voice", "");
string text = CleanText(li.InnerText);
VoiceGroup targetVoice = null;
if (!hasVoiceTabs)
{
// Фільм: вкладок голосів нема — кожен li це окремий стрім (версія)
// Групуємо за data-voice або створюємо нову групу
string groupName = !string.IsNullOrEmpty(voiceAttr) ? voiceAttr : text;
targetVoice = voices.FirstOrDefault(v =>
v.Name.Equals(groupName, StringComparison.OrdinalIgnoreCase));
if (targetVoice == null)
{
targetVoice = new VoiceGroup
{
Name = groupName,
DataId = dataId,
Episodes = new List<EpisodeItem>()
};
voices.Add(targetVoice);
}
}
else
{
if (!string.IsNullOrEmpty(dataId))
targetVoice = voices.FirstOrDefault(v => v.DataId == dataId);
if (targetVoice == null && !string.IsNullOrEmpty(voiceAttr))
targetVoice = voices.FirstOrDefault(v =>
v.Name.Equals(voiceAttr, StringComparison.OrdinalIgnoreCase));
targetVoice ??= voices.FirstOrDefault();
}
int? epNum = ExtractEpisodeNumber(text);
var episode = new EpisodeItem
{
Title = string.IsNullOrEmpty(text) ? $"Епізод {epNum ?? 1}" : text,
FileUrl = NormalizeUrl(fileUrl),
EpisodeNumber = epNum
};
if (targetVoice != null)
targetVoice.Episodes.Add(episode);
}
}
return voices;
}
private List<VoiceGroup> ParseEpisodesFlat(HtmlNode scope)
{
var voices = new List<VoiceGroup>();
var items = scope.SelectNodes("//li[@data-file]");
if (items == null)
return voices;
var defaultVoice = new VoiceGroup
{
Name = "Озвучення",
DataId = "0_0",
Episodes = new List<EpisodeItem>()
};
foreach (var li in items)
{
string fileUrl = li.GetAttributeValue("data-file", "");
string text = CleanText(li.InnerText);
int? epNum = ExtractEpisodeNumber(text);
defaultVoice.Episodes.Add(new EpisodeItem
{
Title = string.IsNullOrEmpty(text) ? "Фільм" : text,
FileUrl = NormalizeUrl(fileUrl),
EpisodeNumber = epNum
});
}
if (defaultVoice.Episodes.Count > 0)
voices.Add(defaultVoice);
return voices;
}
// ===================== Допоміжні методи =====================
private static string BuildSearchQuery(string title, string original_title, string imdb_id)
{
if (!string.IsNullOrEmpty(imdb_id) && imdb_id.StartsWith("tt"))
return imdb_id;
if (!string.IsNullOrEmpty(title))
return title;
if (!string.IsNullOrEmpty(original_title))
return original_title;
return null;
}
private string NormalizeUrl(string url)
{
if (string.IsNullOrEmpty(url))
return string.Empty;
if (url.StartsWith("//"))
return $"https:{url}";
if (url.StartsWith("/"))
return $"{_init.host}{url}";
return url;
}
private static int? ExtractEpisodeNumber(string title)
{
if (string.IsNullOrEmpty(title))
return null;
var match = Regex.Match(title, @"(\d+)");
if (match.Success && int.TryParse(match.Groups[1].Value, out int value))
return value;
return null;
}
private static string CleanText(string value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
return HtmlEntity.DeEntitize(value).Trim();
}
private Task<string> HttpGet(string url, List<HeadersModel> headers)
{
if (_httpHydra != null)
return _httpHydra.Get(url, newheaders: headers);
return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
}
public static TimeSpan cacheTime(int multiaccess, OnlinesSettings init = null)
{
int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess;
if (ctime > multiaccess)
ctime = multiaccess;
return TimeSpan.FromMinutes(ctime);
}
}
}

View File

@ -1,12 +0,0 @@
{
"enable": true,
"version": 3,
"initspace": "LME.UAKino.ModInit",
"online": "LME.UAKino.OnlineApi",
"syntaxPaths": [
"../LME.Shared/GlobalUsings.cs",
"../LME.Shared/Online/OnlineRegistry.cs",
"../LME.Shared/Update/ModuleUpdateService.cs",
"../LME.Shared/Apn/ApnHelper.cs"
]
}