mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-06-17 20:18:53 +00:00
Compare commits
No commits in common. "3c475352fe8a8e7691ad1b96d6eec52fb0646d29" and "adfa97e810d98cb65fb8275b0c78062063e1e245" have entirely different histories.
3c475352fe
...
adfa97e810
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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 недоступний
|
|
||||||
/// Парсить <link itemprop="video" value="..."> або <iframe id="pre" src="...">
|
|
||||||
/// </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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user