mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-04-16 09:22:21 +00:00
feat(uafilmme): add UafilmME streaming plugin with APN support
Integrate a new online streaming source for UafilmME, including API invocation, search, and playback functionality. Adds APN proxy helper for Ashdi streams, module initialization, and related models and controllers to extend the existing online framework.
This commit is contained in:
parent
0aed459fab
commit
b1a7ce510d
99
UafilmME/ApnHelper.cs
Normal file
99
UafilmME/ApnHelper.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Shared.Models.Base;
|
||||
using System;
|
||||
using System.Web;
|
||||
|
||||
namespace Shared.Engine
|
||||
{
|
||||
public static class ApnHelper
|
||||
{
|
||||
public const string DefaultHost = "https://tut.im/proxy.php?url={encodeurl}";
|
||||
|
||||
public static bool TryGetInitConf(JObject conf, out bool enabled, out string host)
|
||||
{
|
||||
enabled = false;
|
||||
host = null;
|
||||
|
||||
if (conf == null)
|
||||
return false;
|
||||
|
||||
if (!conf.TryGetValue("apn", out var apnToken) || apnToken?.Type != JTokenType.Boolean)
|
||||
return false;
|
||||
|
||||
enabled = apnToken.Value<bool>();
|
||||
host = conf.Value<string>("apn_host");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void ApplyInitConf(bool enabled, string host, BaseSettings init)
|
||||
{
|
||||
if (init == null)
|
||||
return;
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
init.apnstream = false;
|
||||
init.apn = null;
|
||||
return;
|
||||
}
|
||||
|
||||
host = NormalizeHost(host);
|
||||
if (host == null)
|
||||
{
|
||||
init.apnstream = false;
|
||||
init.apn = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (init.apn == null)
|
||||
init.apn = new ApnConf();
|
||||
|
||||
init.apn.host = host;
|
||||
init.apnstream = true;
|
||||
}
|
||||
|
||||
public static bool IsEnabled(BaseSettings init)
|
||||
{
|
||||
return init?.apnstream == true && !string.IsNullOrWhiteSpace(init?.apn?.host);
|
||||
}
|
||||
|
||||
public static bool IsAshdiUrl(string url)
|
||||
{
|
||||
return !string.IsNullOrEmpty(url) &&
|
||||
url.IndexOf("ashdi.vip", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
public static string WrapUrl(BaseSettings init, string url)
|
||||
{
|
||||
if (!IsEnabled(init))
|
||||
return url;
|
||||
|
||||
return BuildUrl(init.apn.host, url);
|
||||
}
|
||||
|
||||
public static string BuildUrl(string host, string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(url))
|
||||
return url;
|
||||
|
||||
if (host.Contains("{encodeurl}"))
|
||||
return host.Replace("{encodeurl}", HttpUtility.UrlEncode(url));
|
||||
|
||||
if (host.Contains("{encode_uri}"))
|
||||
return host.Replace("{encode_uri}", HttpUtility.UrlEncode(url));
|
||||
|
||||
if (host.Contains("{uri}"))
|
||||
return host.Replace("{uri}", url);
|
||||
|
||||
return $"{host.TrimEnd('/')}/{url}";
|
||||
}
|
||||
|
||||
private static string NormalizeHost(string host)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
return null;
|
||||
|
||||
return host.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
356
UafilmME/Controller.cs
Normal file
356
UafilmME/Controller.cs
Normal file
@ -0,0 +1,356 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Online.Settings;
|
||||
using Shared.Models.Templates;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using UafilmME.Models;
|
||||
|
||||
namespace UafilmME.Controllers
|
||||
{
|
||||
public class Controller : BaseOnlineController
|
||||
{
|
||||
ProxyManager proxyManager;
|
||||
|
||||
public Controller() : base(ModInit.Settings)
|
||||
{
|
||||
proxyManager = new ProxyManager(ModInit.UafilmME);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("lite/uafilmme")]
|
||||
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.UafilmME);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
var invoke = new UafilmMEInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
|
||||
if (checksearch)
|
||||
{
|
||||
if (!IsCheckOnlineSearchEnabled())
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
|
||||
var searchResults = await invoke.Search(title, original_title, year);
|
||||
if (searchResults != null && searchResults.Count > 0)
|
||||
return Content("data-json=", "text/plain; charset=utf-8");
|
||||
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
}
|
||||
|
||||
long titleId = 0;
|
||||
long.TryParse(href, out titleId);
|
||||
|
||||
if (titleId <= 0)
|
||||
{
|
||||
var searchResults = await invoke.Search(title, original_title, year);
|
||||
if (searchResults == null || searchResults.Count == 0)
|
||||
{
|
||||
OnLog("UafilmME: пошук нічого не повернув.");
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var best = invoke.SelectBestSearchResult(searchResults, id, imdb_id, title, original_title, year, serial);
|
||||
var ordered = searchResults
|
||||
.OrderByDescending(r => r.MatchScore)
|
||||
.ThenByDescending(r => r.Year)
|
||||
.ToList();
|
||||
|
||||
var second = ordered.Skip(1).FirstOrDefault();
|
||||
if (!IsConfidentMatch(best, second, id, imdb_id, serial))
|
||||
{
|
||||
var similarTpl = new SimilarTpl(ordered.Count);
|
||||
foreach (var item in ordered.Take(60))
|
||||
{
|
||||
string details = item.IsSeries ? "Серіал" : "Фільм";
|
||||
string itemYear = item.Year > 1900 ? item.Year.ToString() : string.Empty;
|
||||
string link = $"{host}/lite/uafilmme?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={item.Id}";
|
||||
similarTpl.Append(item.Name, itemYear, details, link, item.Poster);
|
||||
}
|
||||
|
||||
OnLog($"UafilmME: кілька схожих збігів, повертаю SimilarTpl ({ordered.Count}).");
|
||||
return rjson
|
||||
? Content(similarTpl.ToJson(), "application/json; charset=utf-8")
|
||||
: Content(similarTpl.ToHtml(), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
titleId = best?.Id ?? 0;
|
||||
}
|
||||
|
||||
if (titleId <= 0)
|
||||
{
|
||||
OnLog("UafilmME: не вдалося визначити title_id.");
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
}
|
||||
|
||||
if (serial == 1)
|
||||
{
|
||||
if (s == -1)
|
||||
{
|
||||
var seasons = await invoke.GetAllSeasons(titleId);
|
||||
if (seasons == null || seasons.Count == 0)
|
||||
{
|
||||
OnLog($"UafilmME: сезони не знайдено для title_id={titleId}.");
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var seasonTpl = new SeasonTpl(seasons.Count);
|
||||
foreach (var season in seasons)
|
||||
{
|
||||
string seasonName = season.EpisodesCount > 0
|
||||
? $"Сезон {season.Number} ({season.EpisodesCount} еп.)"
|
||||
: $"Сезон {season.Number}";
|
||||
|
||||
string link = $"{host}/lite/uafilmme?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season.Number}&href={titleId}";
|
||||
seasonTpl.Append(seasonName, link, season.Number.ToString());
|
||||
}
|
||||
|
||||
return rjson
|
||||
? Content(seasonTpl.ToJson(), "application/json; charset=utf-8")
|
||||
: Content(seasonTpl.ToHtml(), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
if (s <= 0)
|
||||
{
|
||||
OnLog($"UafilmME: некоректний номер сезону s={s}.");
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var episodes = await invoke.GetSeasonEpisodes(titleId, s);
|
||||
if (episodes == null || episodes.Count == 0)
|
||||
{
|
||||
OnLog($"UafilmME: епізоди не знайдено для title_id={titleId}, season={s}.");
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var episodeTpl = new EpisodeTpl();
|
||||
int appended = 0;
|
||||
int fallbackEpisodeNumber = 1;
|
||||
|
||||
foreach (var episode in episodes)
|
||||
{
|
||||
if (episode.PrimaryVideoId <= 0)
|
||||
continue;
|
||||
|
||||
int episodeNumber = episode.EpisodeNumber > 0 ? episode.EpisodeNumber : fallbackEpisodeNumber;
|
||||
string episodeName = !string.IsNullOrWhiteSpace(episode.Name)
|
||||
? episode.Name
|
||||
: $"Епізод {episodeNumber}";
|
||||
|
||||
string callUrl = $"{host}/lite/uafilmme/play?video_id={episode.PrimaryVideoId}&title_id={titleId}&s={s}&e={episodeNumber}&title={HttpUtility.UrlEncode(title ?? original_title)}";
|
||||
episodeTpl.Append(episodeName, title ?? original_title, s.ToString(), episodeNumber.ToString("D2"), accsArgs(callUrl), "call");
|
||||
|
||||
fallbackEpisodeNumber = Math.Max(fallbackEpisodeNumber, episodeNumber + 1);
|
||||
appended++;
|
||||
}
|
||||
|
||||
if (appended == 0)
|
||||
{
|
||||
OnLog($"UafilmME: у сезоні {s} немає епізодів з playable video_id.");
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
}
|
||||
|
||||
return rjson
|
||||
? Content(episodeTpl.ToJson(), "application/json; charset=utf-8")
|
||||
: Content(episodeTpl.ToHtml(), "text/html; charset=utf-8");
|
||||
}
|
||||
else
|
||||
{
|
||||
var videos = await invoke.GetMovieVideos(titleId);
|
||||
if (videos == null || videos.Count == 0)
|
||||
{
|
||||
OnLog($"UafilmME: не знайдено відео для фільму title_id={titleId}.");
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var movieTpl = new MovieTpl(title, original_title, videos.Count);
|
||||
int index = 1;
|
||||
foreach (var video in videos)
|
||||
{
|
||||
string label = BuildVideoLabel(video, index);
|
||||
string callUrl = $"{host}/lite/uafilmme/play?video_id={video.Id}&title_id={titleId}&title={HttpUtility.UrlEncode(title ?? original_title)}";
|
||||
movieTpl.Append(label, accsArgs(callUrl), "call");
|
||||
index++;
|
||||
}
|
||||
|
||||
return rjson
|
||||
? Content(movieTpl.ToJson(), "application/json; charset=utf-8")
|
||||
: Content(movieTpl.ToHtml(), "text/html; charset=utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("lite/uafilmme/play")]
|
||||
async public Task<ActionResult> Play(long video_id, long title_id = 0, int s = 0, int e = 0, string title = null)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
if (video_id <= 0)
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
|
||||
var init = loadKit(ModInit.UafilmME);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
var invoke = new UafilmMEInvoke(init, hybridCache, OnLog, proxyManager, httpHydra);
|
||||
var watch = await invoke.GetWatch(video_id);
|
||||
var videos = invoke.CollectPlayableVideos(watch);
|
||||
if (videos == null || videos.Count == 0)
|
||||
{
|
||||
OnLog($"UafilmME Play: watch/{video_id} не повернув playable stream.");
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var headers = new List<HeadersModel>()
|
||||
{
|
||||
new HeadersModel("User-Agent", "Mozilla/5.0"),
|
||||
new HeadersModel("Referer", init.host)
|
||||
};
|
||||
|
||||
var streamQuality = new StreamQualityTpl();
|
||||
foreach (var video in videos)
|
||||
{
|
||||
string streamUrl = BuildStreamUrl(init, video.Src, headers, forceProxy: true);
|
||||
if (string.IsNullOrWhiteSpace(streamUrl))
|
||||
continue;
|
||||
|
||||
string label = BuildVideoLabel(video, 0);
|
||||
streamQuality.Append(streamUrl, label);
|
||||
}
|
||||
|
||||
var first = streamQuality.Firts();
|
||||
if (string.IsNullOrWhiteSpace(first.link))
|
||||
{
|
||||
OnLog($"UafilmME Play: не вдалося зібрати streamquality для video_id={video_id}.");
|
||||
return OnError("uafilmme", refresh_proxy: true);
|
||||
}
|
||||
|
||||
string videoTitle = !string.IsNullOrWhiteSpace(title)
|
||||
? title
|
||||
: videos.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v.Name))?.Name ?? string.Empty;
|
||||
|
||||
return UpdateService.Validate(
|
||||
Content(
|
||||
VideoTpl.ToJson("play", first.link, videoTitle, streamquality: streamQuality),
|
||||
"application/json; charset=utf-8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
string BuildStreamUrl(OnlinesSettings init, string streamLink, List<HeadersModel> headers, bool forceProxy)
|
||||
{
|
||||
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, headers: headers, force_streamproxy: forceProxy, proxy: proxyManager.Get());
|
||||
}
|
||||
|
||||
return HostStreamProxy(init, link, headers: headers, force_streamproxy: forceProxy, proxy: proxyManager.Get());
|
||||
}
|
||||
|
||||
private static bool IsConfidentMatch(UafilmSearchItem best, UafilmSearchItem second, long tmdbId, string imdbId, int serial)
|
||||
{
|
||||
if (best == null)
|
||||
return false;
|
||||
|
||||
bool sameTmdb = tmdbId > 0 && best.TmdbId == tmdbId;
|
||||
bool sameImdb = !string.IsNullOrWhiteSpace(imdbId)
|
||||
&& !string.IsNullOrWhiteSpace(best.ImdbId)
|
||||
&& string.Equals(best.ImdbId.Trim(), imdbId.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (sameTmdb || sameImdb)
|
||||
return true;
|
||||
|
||||
if (serial == 1 && !best.IsSeries)
|
||||
return false;
|
||||
|
||||
int secondScore = second?.MatchScore ?? int.MinValue;
|
||||
return best.MatchScore >= 65 && best.MatchScore - secondScore >= 10;
|
||||
}
|
||||
|
||||
private static string BuildVideoLabel(UafilmVideoItem video, int index)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(video?.Name))
|
||||
parts.Add(video.Name.Trim());
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(video?.Quality))
|
||||
parts.Add(video.Quality.Trim());
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(video?.Language))
|
||||
parts.Add(video.Language.Trim());
|
||||
|
||||
if (parts.Count == 0)
|
||||
return index > 0 ? $"Варіант {index}" : "Потік";
|
||||
|
||||
return string.Join(" • ", parts.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
4
UafilmME/GlobalUsings.cs
Normal file
4
UafilmME/GlobalUsings.cs
Normal file
@ -0,0 +1,4 @@
|
||||
global using Shared.Services;
|
||||
global using Shared.Services.Hybrid;
|
||||
global using Shared.Models.Base;
|
||||
global using AppInit = Shared.CoreInit;
|
||||
228
UafilmME/ModInit.cs
Normal file
228
UafilmME/ModInit.cs
Normal file
@ -0,0 +1,228 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using Shared.Models.Online.Settings;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UafilmME
|
||||
{
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 1.0;
|
||||
|
||||
public static OnlinesSettings UafilmME;
|
||||
public static bool ApnHostProvided;
|
||||
|
||||
public static OnlinesSettings Settings
|
||||
{
|
||||
get => UafilmME;
|
||||
set => UafilmME = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Модуль завантажено.
|
||||
/// </summary>
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
UafilmME = new OnlinesSettings("UafilmME", "https://uafilm.me", streamproxy: false, useproxy: false)
|
||||
{
|
||||
displayname = "UAFilmME",
|
||||
displayindex = 0,
|
||||
proxy = new Shared.Models.Base.ProxySettings()
|
||||
{
|
||||
useAuth = true,
|
||||
username = "",
|
||||
password = "",
|
||||
list = new string[] { "socks5://ip:port" }
|
||||
}
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Init("UafilmME", JObject.FromObject(UafilmME));
|
||||
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||
conf.Remove("apn");
|
||||
conf.Remove("apn_host");
|
||||
UafilmME = conf.ToObject<OnlinesSettings>();
|
||||
if (hasApn)
|
||||
ApnHelper.ApplyInitConf(apnEnabled, apnHost, UafilmME);
|
||||
|
||||
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
|
||||
if (hasApn && apnEnabled)
|
||||
{
|
||||
UafilmME.streamproxy = false;
|
||||
}
|
||||
else if (UafilmME.streamproxy)
|
||||
{
|
||||
UafilmME.apnstream = false;
|
||||
UafilmME.apn = null;
|
||||
}
|
||||
|
||||
RegisterWithSearch("uafilmme");
|
||||
}
|
||||
|
||||
private static void RegisterWithSearch(string plugin)
|
||||
{
|
||||
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 withSearchProp = conf?.GetType().GetProperty("with_search", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (withSearchProp?.GetValue(conf) is System.Collections.IList list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item?.ToString(), plugin, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(plugin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static class UpdateService
|
||||
{
|
||||
private static readonly string _connectUrl = "https://lmcuk.lme.isroot.in/stats";
|
||||
|
||||
private static ConnectResponse? Connect = null;
|
||||
private static DateTime? _connectTime = null;
|
||||
private static DateTime? _disconnectTime = null;
|
||||
|
||||
private static readonly TimeSpan _resetInterval = TimeSpan.FromHours(4);
|
||||
private static Timer? _resetTimer = null;
|
||||
|
||||
private static readonly object _lock = new();
|
||||
|
||||
public static async Task ConnectAsync(string host, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_connectTime is not null || Connect?.IsUpdateUnavailable == true)
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_connectTime is not null || Connect?.IsUpdateUnavailable == true)
|
||||
return;
|
||||
|
||||
_connectTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var handler = new SocketsHttpHandler
|
||||
{
|
||||
SslOptions = new SslClientAuthenticationOptions
|
||||
{
|
||||
RemoteCertificateValidationCallback = (_, _, _, _) => true,
|
||||
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
|
||||
}
|
||||
};
|
||||
|
||||
using var client = new HttpClient(handler);
|
||||
client.Timeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var request = new
|
||||
{
|
||||
Host = host,
|
||||
Module = ModInit.Settings.plugin,
|
||||
Version = ModInit.Version,
|
||||
};
|
||||
|
||||
var requestJson = JsonConvert.SerializeObject(request, Formatting.None);
|
||||
var requestContent = new StringContent(requestJson, Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
|
||||
var response = await client
|
||||
.PostAsync(_connectUrl, requestContent, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
if (response.Content.Headers.ContentLength > 0)
|
||||
{
|
||||
var responseText = await response.Content
|
||||
.ReadAsStringAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Connect = JsonConvert.DeserializeObject<ConnectResponse>(responseText);
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_resetTimer?.Dispose();
|
||||
_resetTimer = null;
|
||||
|
||||
if (Connect?.IsUpdateUnavailable != true)
|
||||
{
|
||||
_resetTimer = new Timer(ResetConnectTime, null, _resetInterval, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
_disconnectTime = Connect?.IsNoiseEnabled == true
|
||||
? DateTime.UtcNow.AddHours(Random.Shared.Next(1, 4))
|
||||
: DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ResetConnectTime(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetConnectTime(object? state)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_connectTime = null;
|
||||
Connect = null;
|
||||
|
||||
_resetTimer?.Dispose();
|
||||
_resetTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsDisconnected()
|
||||
{
|
||||
return _disconnectTime is not null
|
||||
&& DateTime.UtcNow >= _disconnectTime;
|
||||
}
|
||||
|
||||
public static ActionResult Validate(ActionResult result)
|
||||
{
|
||||
return IsDisconnected()
|
||||
? throw new JsonReaderException($"Disconnect error: {Guid.CreateVersion7()}")
|
||||
: result;
|
||||
}
|
||||
}
|
||||
|
||||
public record ConnectResponse(bool IsUpdateUnavailable, bool IsNoiseEnabled);
|
||||
}
|
||||
67
UafilmME/Models/UafilmModels.cs
Normal file
67
UafilmME/Models/UafilmModels.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UafilmME.Models
|
||||
{
|
||||
public class UafilmSearchItem
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string OriginalTitle { get; set; }
|
||||
public bool IsSeries { get; set; }
|
||||
public int Year { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public long TmdbId { get; set; }
|
||||
public string Poster { get; set; }
|
||||
public int MatchScore { get; set; }
|
||||
}
|
||||
|
||||
public class UafilmTitleDetails
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string OriginalTitle { get; set; }
|
||||
public bool IsSeries { get; set; }
|
||||
public int Year { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public long TmdbId { get; set; }
|
||||
public int SeasonsCount { get; set; }
|
||||
public long PrimaryVideoId { get; set; }
|
||||
}
|
||||
|
||||
public class UafilmSeasonItem
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public int Number { get; set; }
|
||||
public int EpisodesCount { get; set; }
|
||||
}
|
||||
|
||||
public class UafilmEpisodeItem
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int SeasonNumber { get; set; }
|
||||
public int EpisodeNumber { get; set; }
|
||||
public long PrimaryVideoId { get; set; }
|
||||
public string PrimaryVideoName { get; set; }
|
||||
}
|
||||
|
||||
public class UafilmVideoItem
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Src { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Quality { get; set; }
|
||||
public string Origin { get; set; }
|
||||
public string Language { get; set; }
|
||||
public int? SeasonNum { get; set; }
|
||||
public int? EpisodeNum { get; set; }
|
||||
public long EpisodeId { get; set; }
|
||||
}
|
||||
|
||||
public class UafilmWatchInfo
|
||||
{
|
||||
public UafilmVideoItem Video { get; set; }
|
||||
public List<UafilmVideoItem> AlternativeVideos { get; set; } = new();
|
||||
}
|
||||
}
|
||||
33
UafilmME/OnlineApi.cs
Normal file
33
UafilmME/OnlineApi.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UafilmME
|
||||
{
|
||||
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.UafilmME;
|
||||
if (init.enable && !init.rip)
|
||||
{
|
||||
if (UpdateService.IsDisconnected())
|
||||
init.overridehost = null;
|
||||
|
||||
online.Add(new ModuleOnlineItem(init, "uafilmme"));
|
||||
}
|
||||
|
||||
return online;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
UafilmME/UafilmME.csproj
Normal file
15
UafilmME/UafilmME.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>
|
||||
751
UafilmME/UafilmMEInvoke.cs
Normal file
751
UafilmME/UafilmMEInvoke.cs
Normal file
@ -0,0 +1,751 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Online.Settings;
|
||||
using UafilmME.Models;
|
||||
|
||||
namespace UafilmME
|
||||
{
|
||||
public class UafilmMEInvoke
|
||||
{
|
||||
private readonly OnlinesSettings _init;
|
||||
private readonly IHybridCache _hybridCache;
|
||||
private readonly Action<string> _onLog;
|
||||
private readonly ProxyManager _proxyManager;
|
||||
private readonly HttpHydra _httpHydra;
|
||||
|
||||
|
||||
|
||||
public UafilmMEInvoke(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<UafilmSearchItem>> Search(string title, string originalTitle, int year)
|
||||
{
|
||||
var queries = BuildSearchQueries(title, originalTitle, year).ToList();
|
||||
if (queries.Count == 0)
|
||||
return new List<UafilmSearchItem>();
|
||||
|
||||
var all = new Dictionary<long, UafilmSearchItem>();
|
||||
foreach (var query in queries)
|
||||
{
|
||||
var items = await SearchByQuery(query);
|
||||
foreach (var item in items)
|
||||
all[item.Id] = item;
|
||||
}
|
||||
|
||||
return all.Values.ToList();
|
||||
}
|
||||
|
||||
public UafilmSearchItem SelectBestSearchResult(List<UafilmSearchItem> results, long tmdbId, string imdbId, string title, string originalTitle, int year, int serial)
|
||||
{
|
||||
if (results == null || results.Count == 0)
|
||||
return null;
|
||||
|
||||
foreach (var item in results)
|
||||
item.MatchScore = CalcMatchScore(item, tmdbId, imdbId, title, originalTitle, year, serial);
|
||||
|
||||
return results
|
||||
.OrderByDescending(r => r.MatchScore)
|
||||
.ThenByDescending(r => r.Year)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<UafilmTitleDetails> GetTitleDetails(long titleId)
|
||||
{
|
||||
string memKey = $"UafilmME:title:{titleId}";
|
||||
if (_hybridCache.TryGetValue(memKey, out UafilmTitleDetails cached))
|
||||
return cached;
|
||||
|
||||
try
|
||||
{
|
||||
string json = await ApiGet($"titles/{titleId}?loader=titlePage", $"{_init.host}/titles/{titleId}");
|
||||
var title = ParseTitleDetails(json);
|
||||
if (title != null)
|
||||
_hybridCache.Set(memKey, title, cacheTime(30, init: _init));
|
||||
|
||||
return title;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onLog?.Invoke($"UafilmME: помилка отримання title {titleId}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<UafilmSeasonItem>> GetAllSeasons(long titleId)
|
||||
{
|
||||
string memKey = $"UafilmME:seasons:{titleId}";
|
||||
if (_hybridCache.TryGetValue(memKey, out List<UafilmSeasonItem> cached))
|
||||
return cached;
|
||||
|
||||
var all = new List<UafilmSeasonItem>();
|
||||
int currentPage = 1;
|
||||
int guard = 0;
|
||||
|
||||
while (currentPage > 0 && guard < 100)
|
||||
{
|
||||
guard++;
|
||||
var page = await GetSeasonsPage(titleId, currentPage);
|
||||
if (page.Items.Count == 0)
|
||||
break;
|
||||
|
||||
all.AddRange(page.Items);
|
||||
|
||||
if (page.NextPage.HasValue && page.NextPage.Value != currentPage)
|
||||
currentPage = page.NextPage.Value;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
var result = all
|
||||
.GroupBy(s => s.Number)
|
||||
.Select(g => g.OrderByDescending(x => x.EpisodesCount).First())
|
||||
.OrderBy(s => s.Number)
|
||||
.ToList();
|
||||
|
||||
if (result.Count == 0)
|
||||
{
|
||||
var title = await GetTitleDetails(titleId);
|
||||
if (title?.SeasonsCount > 0)
|
||||
{
|
||||
for (int i = 1; i <= title.SeasonsCount; i++)
|
||||
{
|
||||
result.Add(new UafilmSeasonItem()
|
||||
{
|
||||
Number = i,
|
||||
EpisodesCount = 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Count > 0)
|
||||
_hybridCache.Set(memKey, result, cacheTime(60, init: _init));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<UafilmEpisodeItem>> GetSeasonEpisodes(long titleId, int season)
|
||||
{
|
||||
string memKey = $"UafilmME:episodes:{titleId}:{season}";
|
||||
if (_hybridCache.TryGetValue(memKey, out List<UafilmEpisodeItem> cached))
|
||||
return cached;
|
||||
|
||||
var all = new List<UafilmEpisodeItem>();
|
||||
int currentPage = 1;
|
||||
int guard = 0;
|
||||
|
||||
while (currentPage > 0 && guard < 200)
|
||||
{
|
||||
guard++;
|
||||
var page = await GetEpisodesPage(titleId, season, currentPage);
|
||||
if (page.Items.Count == 0)
|
||||
break;
|
||||
|
||||
all.AddRange(page.Items);
|
||||
|
||||
if (page.NextPage.HasValue && page.NextPage.Value != currentPage)
|
||||
currentPage = page.NextPage.Value;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
var result = all
|
||||
.GroupBy(e => e.Id)
|
||||
.Select(g => g.First())
|
||||
.OrderBy(e => e.EpisodeNumber)
|
||||
.ToList();
|
||||
|
||||
if (result.Count > 0)
|
||||
_hybridCache.Set(memKey, result, cacheTime(30, init: _init));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<UafilmVideoItem>> GetMovieVideos(long titleId)
|
||||
{
|
||||
var title = await GetTitleDetails(titleId);
|
||||
if (title == null || title.PrimaryVideoId <= 0)
|
||||
return new List<UafilmVideoItem>();
|
||||
|
||||
var watch = await GetWatch(title.PrimaryVideoId);
|
||||
return CollectPlayableVideos(watch);
|
||||
}
|
||||
|
||||
public async Task<UafilmWatchInfo> GetWatch(long videoId)
|
||||
{
|
||||
if (videoId <= 0)
|
||||
return null;
|
||||
|
||||
string memKey = $"UafilmME:watch:{videoId}";
|
||||
if (_hybridCache.TryGetValue(memKey, out UafilmWatchInfo cached))
|
||||
return cached;
|
||||
|
||||
try
|
||||
{
|
||||
string json = await ApiGet($"watch/{videoId}", _init.host);
|
||||
var watch = ParseWatchInfo(json);
|
||||
if (watch?.Video != null)
|
||||
_hybridCache.Set(memKey, watch, cacheTime(7, init: _init));
|
||||
|
||||
return watch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onLog?.Invoke($"UafilmME: помилка отримання watch/{videoId}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<UafilmVideoItem> CollectPlayableVideos(UafilmWatchInfo watch)
|
||||
{
|
||||
var list = new List<UafilmVideoItem>();
|
||||
if (watch == null)
|
||||
return list;
|
||||
|
||||
if (watch.Video != null)
|
||||
list.Add(watch.Video);
|
||||
|
||||
if (watch.AlternativeVideos != null && watch.AlternativeVideos.Count > 0)
|
||||
list.AddRange(watch.AlternativeVideos);
|
||||
|
||||
return list
|
||||
.Where(v => v != null && v.Id > 0)
|
||||
.Select(v =>
|
||||
{
|
||||
v.Src = NormalizeVideoSource(v.Src);
|
||||
return v;
|
||||
})
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v.Src))
|
||||
.Where(v => !string.Equals(v.Type, "embed", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(v => v.Src.IndexOf("youtube.com", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
.GroupBy(v => v.Id)
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<UafilmSearchItem>> SearchByQuery(string query)
|
||||
{
|
||||
string memKey = $"UafilmME:search:{query}";
|
||||
if (_hybridCache.TryGetValue(memKey, out List<UafilmSearchItem> cached))
|
||||
return cached;
|
||||
|
||||
string encoded = HttpUtility.UrlEncode(query);
|
||||
string json = await ApiGet($"search/{encoded}?loader=searchPage", $"{_init.host}/search/{encoded}");
|
||||
var items = ParseSearchResults(json);
|
||||
|
||||
if (items.Count > 0)
|
||||
_hybridCache.Set(memKey, items, cacheTime(20, init: _init));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private async Task<(List<UafilmSeasonItem> Items, int? NextPage)> GetSeasonsPage(long titleId, int page)
|
||||
{
|
||||
string memKey = $"UafilmME:seasons-page:{titleId}:{page}";
|
||||
if (_hybridCache.TryGetValue(memKey, out List<UafilmSeasonItem> cachedItems) &&
|
||||
_hybridCache.TryGetValue(memKey + ":next", out int? cachedNext))
|
||||
{
|
||||
return (cachedItems, cachedNext);
|
||||
}
|
||||
|
||||
string suffix = page > 1 ? $"?page={page}" : string.Empty;
|
||||
string json = await ApiGet($"titles/{titleId}/seasons{suffix}", $"{_init.host}/titles/{titleId}");
|
||||
var parsed = ParseSeasonsPage(json);
|
||||
|
||||
_hybridCache.Set(memKey, parsed.Items, cacheTime(30, init: _init));
|
||||
_hybridCache.Set(memKey + ":next", parsed.NextPage, cacheTime(30, init: _init));
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private async Task<(List<UafilmEpisodeItem> Items, int? NextPage)> GetEpisodesPage(long titleId, int season, int page)
|
||||
{
|
||||
string memKey = $"UafilmME:episodes-page:{titleId}:{season}:{page}";
|
||||
if (_hybridCache.TryGetValue(memKey, out List<UafilmEpisodeItem> cachedItems) &&
|
||||
_hybridCache.TryGetValue(memKey + ":next", out int? cachedNext))
|
||||
{
|
||||
return (cachedItems, cachedNext);
|
||||
}
|
||||
|
||||
string suffix = page > 1 ? $"?page={page}" : string.Empty;
|
||||
string json = await ApiGet($"titles/{titleId}/seasons/{season}/episodes{suffix}", $"{_init.host}/titles/{titleId}");
|
||||
var parsed = ParseEpisodesPage(json);
|
||||
|
||||
_hybridCache.Set(memKey, parsed.Items, cacheTime(20, init: _init));
|
||||
_hybridCache.Set(memKey + ":next", parsed.NextPage, cacheTime(20, init: _init));
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private async Task<string> ApiGet(string pathAndQuery, string referer)
|
||||
{
|
||||
string url = $"{_init.host.TrimEnd('/')}/api/v1/{pathAndQuery.TrimStart('/')}";
|
||||
string reqReferer = string.IsNullOrWhiteSpace(referer) ? $"{_init.host}/" : referer;
|
||||
|
||||
var headers = new List<HeadersModel>()
|
||||
{
|
||||
new HeadersModel("User-Agent", "EchoapiRuntime/1.1.0"),
|
||||
new HeadersModel("Referer", reqReferer),
|
||||
new HeadersModel("Accept", "*/*")
|
||||
};
|
||||
|
||||
if (_httpHydra != null)
|
||||
return await _httpHydra.Get(url, newheaders: headers);
|
||||
|
||||
return await Http.Get(url, headers: headers, proxy: _proxyManager.Get());
|
||||
}
|
||||
|
||||
private string NormalizeVideoSource(string src)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(src))
|
||||
return null;
|
||||
|
||||
src = src.Trim();
|
||||
if (src.StartsWith("//"))
|
||||
return "https:" + src;
|
||||
|
||||
if (src.StartsWith("/"))
|
||||
return _init.host.TrimEnd('/') + src;
|
||||
|
||||
return src;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> BuildSearchQueries(string title, string originalTitle, int year)
|
||||
{
|
||||
var queries = new List<string>();
|
||||
void Add(string value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
queries.Add(value.Trim());
|
||||
}
|
||||
|
||||
Add(title);
|
||||
Add(originalTitle);
|
||||
|
||||
if (year > 1900)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
Add($"{title} {year}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(originalTitle))
|
||||
Add($"{originalTitle} {year}");
|
||||
}
|
||||
|
||||
return queries
|
||||
.Where(q => !string.IsNullOrWhiteSpace(q))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private List<UafilmSearchItem> ParseSearchResults(string json)
|
||||
{
|
||||
var list = new List<UafilmSearchItem>();
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return list;
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (!TryGetArray(doc.RootElement, "results", out var results))
|
||||
return list;
|
||||
|
||||
foreach (var item in results.EnumerateArray())
|
||||
{
|
||||
if (!TryReadLong(item, "id", out long id) || id <= 0)
|
||||
continue;
|
||||
|
||||
list.Add(new UafilmSearchItem()
|
||||
{
|
||||
Id = id,
|
||||
Name = ReadString(item, "name"),
|
||||
OriginalTitle = ReadString(item, "original_title"),
|
||||
IsSeries = ReadBool(item, "is_series"),
|
||||
Year = ReadInt(item, "year"),
|
||||
ImdbId = ReadString(item, "imdb_id"),
|
||||
TmdbId = ReadLong(item, "tmdb_id"),
|
||||
Poster = ReadString(item, "poster")
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private UafilmTitleDetails ParseTitleDetails(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return null;
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (!TryGetObject(doc.RootElement, "title", out var titleObj))
|
||||
return null;
|
||||
|
||||
var info = new UafilmTitleDetails()
|
||||
{
|
||||
Id = ReadLong(titleObj, "id"),
|
||||
Name = ReadString(titleObj, "name"),
|
||||
OriginalTitle = ReadString(titleObj, "original_title"),
|
||||
IsSeries = ReadBool(titleObj, "is_series"),
|
||||
Year = ReadInt(titleObj, "year"),
|
||||
ImdbId = ReadString(titleObj, "imdb_id"),
|
||||
TmdbId = ReadLong(titleObj, "tmdb_id"),
|
||||
SeasonsCount = ReadInt(titleObj, "seasons_count")
|
||||
};
|
||||
|
||||
if (TryGetObject(titleObj, "primary_video", out var primaryVideo))
|
||||
info.PrimaryVideoId = ReadLong(primaryVideo, "id");
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private (List<UafilmSeasonItem> Items, int? NextPage) ParseSeasonsPage(string json)
|
||||
{
|
||||
var items = new List<UafilmSeasonItem>();
|
||||
int? next = null;
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return (items, next);
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (!TryGetObject(doc.RootElement, "pagination", out var pagination))
|
||||
return (items, next);
|
||||
|
||||
next = ReadNullableInt(pagination, "next_page");
|
||||
|
||||
if (!TryGetArray(pagination, "data", out var data))
|
||||
return (items, next);
|
||||
|
||||
foreach (var item in data.EnumerateArray())
|
||||
{
|
||||
int number = ReadInt(item, "number");
|
||||
if (number <= 0)
|
||||
continue;
|
||||
|
||||
items.Add(new UafilmSeasonItem()
|
||||
{
|
||||
Id = ReadLong(item, "id"),
|
||||
Number = number,
|
||||
EpisodesCount = ReadInt(item, "episodes_count")
|
||||
});
|
||||
}
|
||||
|
||||
return (items, next);
|
||||
}
|
||||
|
||||
private (List<UafilmEpisodeItem> Items, int? NextPage) ParseEpisodesPage(string json)
|
||||
{
|
||||
var items = new List<UafilmEpisodeItem>();
|
||||
int? next = null;
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return (items, next);
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (!TryGetObject(doc.RootElement, "pagination", out var pagination))
|
||||
return (items, next);
|
||||
|
||||
next = ReadNullableInt(pagination, "next_page");
|
||||
|
||||
if (!TryGetArray(pagination, "data", out var data))
|
||||
return (items, next);
|
||||
|
||||
foreach (var item in data.EnumerateArray())
|
||||
{
|
||||
long episodeId = ReadLong(item, "id");
|
||||
if (episodeId <= 0)
|
||||
continue;
|
||||
|
||||
long primaryVideoId = 0;
|
||||
string primaryVideoName = null;
|
||||
if (TryGetObject(item, "primary_video", out var primaryVideoObj))
|
||||
{
|
||||
primaryVideoId = ReadLong(primaryVideoObj, "id");
|
||||
primaryVideoName = ReadString(primaryVideoObj, "name");
|
||||
}
|
||||
|
||||
items.Add(new UafilmEpisodeItem()
|
||||
{
|
||||
Id = episodeId,
|
||||
Name = ReadString(item, "name"),
|
||||
SeasonNumber = ReadInt(item, "season_number"),
|
||||
EpisodeNumber = ReadInt(item, "episode_number"),
|
||||
PrimaryVideoId = primaryVideoId,
|
||||
PrimaryVideoName = primaryVideoName
|
||||
});
|
||||
}
|
||||
|
||||
return (items, next);
|
||||
}
|
||||
|
||||
private UafilmWatchInfo ParseWatchInfo(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return null;
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
var watch = new UafilmWatchInfo();
|
||||
|
||||
if (TryGetObject(doc.RootElement, "video", out var videoObj))
|
||||
watch.Video = ParseVideo(videoObj);
|
||||
|
||||
if (TryGetArray(doc.RootElement, "alternative_videos", out var alternatives))
|
||||
{
|
||||
foreach (var alt in alternatives.EnumerateArray())
|
||||
{
|
||||
var parsed = ParseVideo(alt);
|
||||
if (parsed != null)
|
||||
watch.AlternativeVideos.Add(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
return watch;
|
||||
}
|
||||
|
||||
private static UafilmVideoItem ParseVideo(JsonElement obj)
|
||||
{
|
||||
long id = ReadLong(obj, "id");
|
||||
if (id <= 0)
|
||||
return null;
|
||||
|
||||
return new UafilmVideoItem()
|
||||
{
|
||||
Id = id,
|
||||
Name = ReadString(obj, "name"),
|
||||
Src = ReadString(obj, "src"),
|
||||
Type = ReadString(obj, "type"),
|
||||
Quality = ReadString(obj, "quality"),
|
||||
Origin = ReadString(obj, "origin"),
|
||||
Language = ReadString(obj, "language"),
|
||||
SeasonNum = ReadNullableInt(obj, "season_num"),
|
||||
EpisodeNum = ReadNullableInt(obj, "episode_num"),
|
||||
EpisodeId = ReadLong(obj, "episode_id")
|
||||
};
|
||||
}
|
||||
|
||||
private int CalcMatchScore(UafilmSearchItem item, long tmdbId, string imdbId, string title, string originalTitle, int year, int serial)
|
||||
{
|
||||
int score = 0;
|
||||
|
||||
if (item == null)
|
||||
return score;
|
||||
|
||||
if (tmdbId > 0 && item.TmdbId == tmdbId)
|
||||
score += 120;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(imdbId) && !string.IsNullOrWhiteSpace(item.ImdbId) && string.Equals(item.ImdbId.Trim(), imdbId.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
score += 120;
|
||||
|
||||
if (serial == 1)
|
||||
score += item.IsSeries ? 25 : -25;
|
||||
else
|
||||
score += item.IsSeries ? -15 : 15;
|
||||
|
||||
if (year > 1900 && item.Year > 1900)
|
||||
{
|
||||
int diff = Math.Abs(item.Year - year);
|
||||
if (diff == 0)
|
||||
score += 20;
|
||||
else if (diff == 1)
|
||||
score += 10;
|
||||
else if (diff == 2)
|
||||
score += 5;
|
||||
else
|
||||
score -= 6;
|
||||
}
|
||||
|
||||
score += ScoreTitle(item.Name, title);
|
||||
score += ScoreTitle(item.Name, originalTitle);
|
||||
score += ScoreTitle(item.OriginalTitle, title);
|
||||
score += ScoreTitle(item.OriginalTitle, originalTitle);
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private static int ScoreTitle(string candidate, string expected)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(candidate) || string.IsNullOrWhiteSpace(expected))
|
||||
return 0;
|
||||
|
||||
string left = NormalizeTitle(candidate);
|
||||
string right = NormalizeTitle(expected);
|
||||
if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right))
|
||||
return 0;
|
||||
|
||||
if (left == right)
|
||||
return 35;
|
||||
|
||||
if (left.Contains(right) || right.Contains(left))
|
||||
return 20;
|
||||
|
||||
var leftWords = left.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var rightWords = right.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
int overlap = leftWords.Intersect(rightWords).Count();
|
||||
if (overlap >= 2)
|
||||
return 12;
|
||||
if (overlap == 1)
|
||||
return 6;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string NormalizeTitle(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return string.Empty;
|
||||
|
||||
string normalized = value.ToLowerInvariant();
|
||||
normalized = Regex.Replace(normalized, "[^\\p{L}\\p{Nd}]+", " ", RegexOptions.CultureInvariant);
|
||||
normalized = Regex.Replace(normalized, "\\s+", " ", RegexOptions.CultureInvariant).Trim();
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static bool TryGetObject(JsonElement source, string property, out JsonElement value)
|
||||
{
|
||||
value = default;
|
||||
if (!source.TryGetProperty(property, out var prop) || prop.ValueKind != JsonValueKind.Object)
|
||||
return false;
|
||||
|
||||
value = prop;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetArray(JsonElement source, string property, out JsonElement value)
|
||||
{
|
||||
value = default;
|
||||
if (!source.TryGetProperty(property, out var prop) || prop.ValueKind != JsonValueKind.Array)
|
||||
return false;
|
||||
|
||||
value = prop;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string ReadString(JsonElement source, string property)
|
||||
{
|
||||
if (!source.TryGetProperty(property, out var value))
|
||||
return null;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.String)
|
||||
return value.GetString();
|
||||
|
||||
if (value.ValueKind == JsonValueKind.Number)
|
||||
return value.GetRawText();
|
||||
|
||||
if (value.ValueKind == JsonValueKind.True)
|
||||
return bool.TrueString;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.False)
|
||||
return bool.FalseString;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ReadBool(JsonElement source, string property)
|
||||
{
|
||||
if (!source.TryGetProperty(property, out var value))
|
||||
return false;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.True)
|
||||
return true;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.False)
|
||||
return false;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.Number)
|
||||
return value.GetInt32() != 0;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
string text = value.GetString();
|
||||
if (bool.TryParse(text, out bool parsedBool))
|
||||
return parsedBool;
|
||||
|
||||
if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedInt))
|
||||
return parsedInt != 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int ReadInt(JsonElement source, string property)
|
||||
{
|
||||
if (!source.TryGetProperty(property, out var value))
|
||||
return 0;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int number))
|
||||
return number;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.String && int.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
|
||||
return parsed;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int? ReadNullableInt(JsonElement source, string property)
|
||||
{
|
||||
if (!source.TryGetProperty(property, out var value))
|
||||
return null;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.Null)
|
||||
return null;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int number))
|
||||
return number;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.String && int.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
|
||||
return parsed;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long ReadLong(JsonElement source, string property)
|
||||
{
|
||||
return TryReadLong(source, property, out long value)
|
||||
? value
|
||||
: 0;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static bool TryReadLong(JsonElement source, string property, out long value)
|
||||
{
|
||||
value = 0;
|
||||
if (!source.TryGetProperty(property, out var element))
|
||||
return false;
|
||||
|
||||
if (element.ValueKind == JsonValueKind.Number && element.TryGetInt64(out long number))
|
||||
{
|
||||
value = number;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (element.ValueKind == JsonValueKind.String && long.TryParse(element.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out long parsed))
|
||||
{
|
||||
value = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
6
UafilmME/manifest.json
Normal file
6
UafilmME/manifest.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "UafilmME.ModInit",
|
||||
"online": "UafilmME.OnlineApi"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user