mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-06-17 12:08:54 +00:00
feat(uakino): add UAKino online source module
Add new online source module for UAKino website providing movie and series search and playback functionality. Includes controller, model definitions, online API integration, search implementation with caching, and module initialization. Implements similar result handling for multiple search results and serial/movie playback differentiation.
This commit is contained in:
parent
adfa97e810
commit
317cb6292c
222
LME.UAKino/Controller.cs
Normal file
222
LME.UAKino/Controller.cs
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
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 (searchResults.Count > 1)
|
||||||
|
{
|
||||||
|
var similar_tpl = new SimilarTpl(searchResults.Count);
|
||||||
|
foreach (var res in searchResults)
|
||||||
|
{
|
||||||
|
string link = $"{host}/lite/lme_uakino?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}";
|
||||||
|
similar_tpl.Append(res.Title, res.Year?.ToString() ?? "", 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
itemUrl = searchResults[0].Url;
|
||||||
|
newsId = searchResults[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)
|
||||||
|
return OnError("lme_uakino", refresh_proxy: true);
|
||||||
|
|
||||||
|
if (serial == 1)
|
||||||
|
{
|
||||||
|
return HandleSerial(init, voices, title, original_title, year, imdb_id, kinopoisk_id, itemUrl, t, rjson);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return HandleMovie(init, voices, title, original_title, rjson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionResult HandleSerial(OnlinesSettings init, List<VoiceGroup> voices, string title, string original_title, int year, string imdb_id, long kinopoisk_id, string itemUrl, string t, bool rjson)
|
||||||
|
{
|
||||||
|
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)}&year={year}&serial=1&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);
|
||||||
|
|
||||||
|
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 streamUrl = BuildStreamUrl(init, ep.FileUrl);
|
||||||
|
episode_tpl.Append(epName, title ?? original_title, "1", 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionResult HandleMovie(OnlinesSettings init, List<VoiceGroup> voices, string title, string original_title, bool rjson)
|
||||||
|
{
|
||||||
|
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 streamUrl = BuildStreamUrl(init, ep.FileUrl);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
LME.UAKino/ModInit.cs
Normal file
99
LME.UAKino/ModInit.cs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
LME.UAKino/Models/UAKinoModels.cs
Normal file
28
LME.UAKino/Models/UAKinoModels.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace LME.UAKino.Models
|
||||||
|
{
|
||||||
|
public class SearchResult
|
||||||
|
{
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
35
LME.UAKino/OnlineApi.cs
Normal file
35
LME.UAKino/OnlineApi.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
LME.UAKino/UAKino.csproj
Normal file
15
LME.UAKino/UAKino.csproj
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<OutputType>library</OutputType>
|
||||||
|
<IsPackable>true</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Shared">
|
||||||
|
<HintPath>..\..\Shared.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
407
LME.UAKino/UAKinoInvoke.cs
Normal file
407
LME.UAKino/UAKinoInvoke.cs
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
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 FilterByYear(cached, year);
|
||||||
|
|
||||||
|
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 results = ParseSearchResults(htmlDoc);
|
||||||
|
if (results.Count > 0)
|
||||||
|
_hybridCache.Set(memKey, results, cacheTime(20));
|
||||||
|
|
||||||
|
return FilterByYear(results, year);
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
/// Витягнути 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SearchResult> ParseSearchResults(HtmlDocument doc)
|
||||||
|
{
|
||||||
|
var results = new List<SearchResult>();
|
||||||
|
var nodes = doc.DocumentNode.SelectNodes("//a[@class='search-result-link']");
|
||||||
|
if (nodes == null)
|
||||||
|
return results;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
string newsId = ExtractNewsId(href);
|
||||||
|
|
||||||
|
results.Add(new SearchResult
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
OriginalTitle = origTitle,
|
||||||
|
Url = NormalizeUrl(href),
|
||||||
|
Poster = NormalizeUrl(poster),
|
||||||
|
Year = year,
|
||||||
|
NewsId = newsId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// спроба знайти епізоди без обгортки playlists-player
|
||||||
|
return ParseEpisodesFlat(doc.DocumentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсимо голоси (озвучки) з вкладки playlists-lists
|
||||||
|
var voiceItems = playerDiv.SelectNodes(".//div[@class='playlists-lists']//ul/li");
|
||||||
|
if (voiceItems != null)
|
||||||
|
{
|
||||||
|
foreach (var li in voiceItems)
|
||||||
|
{
|
||||||
|
string dataId = li.GetAttributeValue("data-id", "");
|
||||||
|
string text = CleanText(li.InnerText);
|
||||||
|
// Прибираємо "(X-Y)" з кінця назви озвучки
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Визначаємо до якого voice групи належить
|
||||||
|
VoiceGroup targetVoice = null;
|
||||||
|
|
||||||
|
// Спершу за data-id
|
||||||
|
if (!string.IsNullOrEmpty(dataId))
|
||||||
|
targetVoice = voices.FirstOrDefault(v => v.DataId == dataId);
|
||||||
|
|
||||||
|
// Якщо не знайшли, то за data-voice (назвою)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Парсинг коли немає структури playlists-player (наприклад для фільмів)
|
||||||
|
/// </summary>
|
||||||
|
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 voiceAttr = li.GetAttributeValue("data-voice", "");
|
||||||
|
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 static List<SearchResult> FilterByYear(List<SearchResult> results, int year)
|
||||||
|
{
|
||||||
|
if (results == null || results.Count <= 1 || year <= 0)
|
||||||
|
return results;
|
||||||
|
|
||||||
|
var yearMatch = results.Where(r => r.Year == year).ToList();
|
||||||
|
if (yearMatch.Count > 0)
|
||||||
|
return yearMatch;
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
LME.UAKino/manifest.json
Normal file
12
LME.UAKino/manifest.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"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