From 317cb6292c4f9a122b48556ce3f6c212184d846c Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 15 May 2026 18:46:00 +0300 Subject: [PATCH] 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. --- LME.UAKino/Controller.cs | 222 ++++++++++++++++ LME.UAKino/ModInit.cs | 99 ++++++++ LME.UAKino/Models/UAKinoModels.cs | 28 ++ LME.UAKino/OnlineApi.cs | 35 +++ LME.UAKino/UAKino.csproj | 15 ++ LME.UAKino/UAKinoInvoke.cs | 407 ++++++++++++++++++++++++++++++ LME.UAKino/manifest.json | 12 + 7 files changed, 818 insertions(+) create mode 100644 LME.UAKino/Controller.cs create mode 100644 LME.UAKino/ModInit.cs create mode 100644 LME.UAKino/Models/UAKinoModels.cs create mode 100644 LME.UAKino/OnlineApi.cs create mode 100644 LME.UAKino/UAKino.csproj create mode 100644 LME.UAKino/UAKinoInvoke.cs create mode 100644 LME.UAKino/manifest.json diff --git a/LME.UAKino/Controller.cs b/LME.UAKino/Controller.cs new file mode 100644 index 0000000..0374701 --- /dev/null +++ b/LME.UAKino/Controller.cs @@ -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 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 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 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); + } + } +} diff --git a/LME.UAKino/ModInit.cs b/LME.UAKino/ModInit.cs new file mode 100644 index 0000000..2bd0d2d --- /dev/null +++ b/LME.UAKino/ModInit.cs @@ -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; + } + + /// + /// модуль загружен + /// + 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(); + 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); + } +} diff --git a/LME.UAKino/Models/UAKinoModels.cs b/LME.UAKino/Models/UAKinoModels.cs new file mode 100644 index 0000000..bb5943a --- /dev/null +++ b/LME.UAKino/Models/UAKinoModels.cs @@ -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 Episodes { get; set; } = new(); + } + + public class EpisodeItem + { + public string Title { get; set; } + public string FileUrl { get; set; } + public int? EpisodeNumber { get; set; } + } +} diff --git a/LME.UAKino/OnlineApi.cs b/LME.UAKino/OnlineApi.cs new file mode 100644 index 0000000..857a997 --- /dev/null +++ b/LME.UAKino/OnlineApi.cs @@ -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 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 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(); + + var init = ModInit.UAKino; + if (init.enable && !init.rip) + { + if (UpdateService.IsDisconnected()) + init.overridehost = null; + + online.Add(new ModuleOnlineItem(init, "lme_uakino")); + } + + return online; + } + } +} diff --git a/LME.UAKino/UAKino.csproj b/LME.UAKino/UAKino.csproj new file mode 100644 index 0000000..c280999 --- /dev/null +++ b/LME.UAKino/UAKino.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + library + true + + + + + ..\..\Shared.dll + + + + diff --git a/LME.UAKino/UAKinoInvoke.cs b/LME.UAKino/UAKinoInvoke.cs new file mode 100644 index 0000000..cf3c2de --- /dev/null +++ b/LME.UAKino/UAKinoInvoke.cs @@ -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 _onLog; + private readonly ProxyManager _proxyManager; + private readonly HttpHydra _httpHydra; + + public UAKinoInvoke(OnlinesSettings init, IHybridCache hybridCache, Action onLog, ProxyManager proxyManager, HttpHydra httpHydra = null) + { + _init = init; + _hybridCache = hybridCache; + _onLog = onLog; + _proxyManager = proxyManager; + _httpHydra = httpHydra; + } + + public async Task> 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 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() + { + 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; + } + } + + /// + /// Отримати плейлист (озвучки + епізоди) за news_id + /// + public async Task> GetPlaylist(string newsId) + { + if (string.IsNullOrEmpty(newsId)) + return null; + + string memKey = $"UAKino:playlist:{newsId}"; + if (_hybridCache.TryGetValue(memKey, out List 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() + { + 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; + } + } + + /// + /// Витягнути news_id з URL контенту + /// + 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 ParseSearchResults(HtmlDocument doc) + { + var results = new List(); + 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 ParsePlaylistHtml(string html) + { + var voices = new List(); + + 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() + }); + } + } + } + + // Парсимо епізоди з 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; + } + + /// + /// Парсинг коли немає структури playlists-player (наприклад для фільмів) + /// + private List ParseEpisodesFlat(HtmlNode scope) + { + var voices = new List(); + var items = scope.SelectNodes("//li[@data-file]"); + if (items == null) + return voices; + + var defaultVoice = new VoiceGroup + { + Name = "Озвучення", + DataId = "0_0", + Episodes = new List() + }; + + 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 FilterByYear(List 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 HttpGet(string url, List 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); + } + } +} diff --git a/LME.UAKino/manifest.json b/LME.UAKino/manifest.json new file mode 100644 index 0000000..9ed0240 --- /dev/null +++ b/LME.UAKino/manifest.json @@ -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" + ] +}