mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-04-16 17:32:20 +00:00
Compare commits
4 Commits
345705eac2
...
104afc463e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
104afc463e | ||
|
|
32a48d2cf5 | ||
|
|
2e597dec9f | ||
|
|
3081af4dd9 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,3 +10,4 @@
|
|||||||
/.qodo/
|
/.qodo/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
/planing/
|
||||||
86
KlonFUN/ApnHelper.cs
Normal file
86
KlonFUN/ApnHelper.cs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(host))
|
||||||
|
host = DefaultHost;
|
||||||
|
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
236
KlonFUN/Controller.cs
Normal file
236
KlonFUN/Controller.cs
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using KlonFUN.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Shared;
|
||||||
|
using Shared.Engine;
|
||||||
|
using Shared.Models.Online.Settings;
|
||||||
|
using Shared.Models.Templates;
|
||||||
|
|
||||||
|
namespace KlonFUN.Controllers
|
||||||
|
{
|
||||||
|
public class Controller : BaseOnlineController
|
||||||
|
{
|
||||||
|
ProxyManager proxyManager;
|
||||||
|
|
||||||
|
public Controller() : base(ModInit.Settings)
|
||||||
|
{
|
||||||
|
proxyManager = new ProxyManager(ModInit.KlonFUN);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("klonfun")]
|
||||||
|
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 = await loadKit(ModInit.KlonFUN);
|
||||||
|
if (!init.enable)
|
||||||
|
return Forbid();
|
||||||
|
|
||||||
|
var invoke = new KlonFUNInvoke(init, hybridCache, OnLog, proxyManager);
|
||||||
|
|
||||||
|
if (checksearch)
|
||||||
|
{
|
||||||
|
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||||
|
return OnError("klonfun", proxyManager);
|
||||||
|
|
||||||
|
var checkResults = await invoke.Search(imdb_id, title, original_title);
|
||||||
|
if (checkResults != null && checkResults.Count > 0)
|
||||||
|
return Content("data-json=", "text/plain; charset=utf-8");
|
||||||
|
|
||||||
|
return OnError("klonfun", proxyManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
string itemUrl = href;
|
||||||
|
if (string.IsNullOrWhiteSpace(itemUrl))
|
||||||
|
{
|
||||||
|
var searchResults = await invoke.Search(imdb_id, title, original_title);
|
||||||
|
if (searchResults == null || searchResults.Count == 0)
|
||||||
|
return OnError("klonfun", proxyManager);
|
||||||
|
|
||||||
|
if (searchResults.Count > 1)
|
||||||
|
{
|
||||||
|
var similarTpl = new SimilarTpl(searchResults.Count);
|
||||||
|
foreach (SearchResult result in searchResults)
|
||||||
|
{
|
||||||
|
string link = $"{host}/klonfun?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(result.Url)}";
|
||||||
|
similarTpl.Append(result.Title, result.Year > 0 ? result.Year.ToString() : string.Empty, string.Empty, link, result.Poster);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rjson
|
||||||
|
? Content(similarTpl.ToJson(), "application/json; charset=utf-8")
|
||||||
|
: Content(similarTpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
itemUrl = searchResults[0].Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = await invoke.GetItem(itemUrl);
|
||||||
|
if (item == null || string.IsNullOrWhiteSpace(item.PlayerUrl))
|
||||||
|
{
|
||||||
|
OnLog($"KlonFUN: не знайдено iframe-плеєр для {itemUrl}");
|
||||||
|
return OnError("klonfun", proxyManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
string contentTitle = !string.IsNullOrWhiteSpace(title) ? title : item.Title;
|
||||||
|
if (string.IsNullOrWhiteSpace(contentTitle))
|
||||||
|
contentTitle = "KlonFUN";
|
||||||
|
|
||||||
|
string contentOriginalTitle = !string.IsNullOrWhiteSpace(original_title) ? original_title : contentTitle;
|
||||||
|
|
||||||
|
bool isSerial = serial == 1 || item.IsSerialPlayer;
|
||||||
|
if (isSerial)
|
||||||
|
{
|
||||||
|
var serialStructure = await invoke.GetSerialStructure(item.PlayerUrl);
|
||||||
|
if (serialStructure == null || serialStructure.Voices.Count == 0)
|
||||||
|
return OnError("klonfun", proxyManager);
|
||||||
|
|
||||||
|
if (s == -1)
|
||||||
|
{
|
||||||
|
List<int> seasons;
|
||||||
|
if (!string.IsNullOrWhiteSpace(t))
|
||||||
|
{
|
||||||
|
var selectedVoice = serialStructure.Voices.FirstOrDefault(v => v.Key.Equals(t, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (selectedVoice != null)
|
||||||
|
{
|
||||||
|
seasons = selectedVoice.Seasons.Keys.OrderBy(sn => sn).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
seasons = serialStructure.Voices
|
||||||
|
.SelectMany(v => v.Seasons.Keys)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(sn => sn)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
seasons = serialStructure.Voices
|
||||||
|
.SelectMany(v => v.Seasons.Keys)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(sn => sn)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seasons.Count == 0)
|
||||||
|
return OnError("klonfun", proxyManager);
|
||||||
|
|
||||||
|
var seasonTpl = new SeasonTpl(seasons.Count);
|
||||||
|
foreach (int seasonNumber in seasons)
|
||||||
|
{
|
||||||
|
string link = $"{host}/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}&href={HttpUtility.UrlEncode(itemUrl)}";
|
||||||
|
if (!string.IsNullOrWhiteSpace(t))
|
||||||
|
link += $"&t={HttpUtility.UrlEncode(t)}";
|
||||||
|
|
||||||
|
seasonTpl.Append($"{seasonNumber}", link, seasonNumber.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return rjson
|
||||||
|
? Content(seasonTpl.ToJson(), "application/json; charset=utf-8")
|
||||||
|
: Content(seasonTpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
var voicesForSeason = serialStructure.Voices
|
||||||
|
.Where(v => v.Seasons.ContainsKey(s))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (voicesForSeason.Count == 0)
|
||||||
|
return OnError("klonfun", proxyManager);
|
||||||
|
|
||||||
|
var selectedVoiceForSeason = voicesForSeason
|
||||||
|
.FirstOrDefault(v => !string.IsNullOrWhiteSpace(t) && v.Key.Equals(t, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? voicesForSeason[0];
|
||||||
|
|
||||||
|
var voiceTpl = new VoiceTpl(voicesForSeason.Count);
|
||||||
|
foreach (var voice in voicesForSeason)
|
||||||
|
{
|
||||||
|
string voiceLink = $"{host}/klonfun?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.Key)}&href={HttpUtility.UrlEncode(itemUrl)}";
|
||||||
|
voiceTpl.Append(voice.DisplayName, voice.Key.Equals(selectedVoiceForSeason.Key, StringComparison.OrdinalIgnoreCase), voiceLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedVoiceForSeason.Seasons.TryGetValue(s, out List<SerialEpisode> episodes) || episodes.Count == 0)
|
||||||
|
return OnError("klonfun", proxyManager);
|
||||||
|
|
||||||
|
var episodeTpl = new EpisodeTpl(episodes.Count);
|
||||||
|
foreach (SerialEpisode episode in episodes.OrderBy(e => e.Number))
|
||||||
|
{
|
||||||
|
string episodeTitle = !string.IsNullOrWhiteSpace(episode.Title)
|
||||||
|
? episode.Title
|
||||||
|
: $"Серія {episode.Number}";
|
||||||
|
|
||||||
|
string streamUrl = BuildStreamUrl(init, episode.Link);
|
||||||
|
episodeTpl.Append(episodeTitle, contentTitle, s.ToString(), episode.Number.ToString("D2"), streamUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
episodeTpl.Append(voiceTpl);
|
||||||
|
if (rjson)
|
||||||
|
return Content(episodeTpl.ToJson(), "application/json; charset=utf-8");
|
||||||
|
|
||||||
|
return Content(episodeTpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var streams = await invoke.GetMovieStreams(item.PlayerUrl);
|
||||||
|
if (streams == null || streams.Count == 0)
|
||||||
|
return OnError("klonfun", proxyManager);
|
||||||
|
|
||||||
|
var movieTpl = new MovieTpl(contentTitle, contentOriginalTitle, streams.Count);
|
||||||
|
for (int i = 0; i < streams.Count; i++)
|
||||||
|
{
|
||||||
|
var stream = streams[i];
|
||||||
|
string label = !string.IsNullOrWhiteSpace(stream.Title)
|
||||||
|
? stream.Title
|
||||||
|
: $"Варіант {i + 1}";
|
||||||
|
|
||||||
|
string streamUrl = BuildStreamUrl(init, stream.Link);
|
||||||
|
movieTpl.Append(label, streamUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rjson
|
||||||
|
? Content(movieTpl.ToJson(), "application/json; charset=utf-8")
|
||||||
|
: Content(movieTpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string BuildStreamUrl(OnlinesSettings init, string streamLink)
|
||||||
|
{
|
||||||
|
string link = StripLampacArgs(streamLink?.Trim());
|
||||||
|
if (string.IsNullOrWhiteSpace(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.IsNullOrWhiteSpace(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
KlonFUN/KlonFUN.csproj
Normal file
15
KlonFUN/KlonFUN.csproj
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<OutputType>library</OutputType>
|
||||||
|
<IsPackable>true</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Shared">
|
||||||
|
<HintPath>..\..\Shared.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
654
KlonFUN/KlonFUNInvoke.cs
Normal file
654
KlonFUN/KlonFUNInvoke.cs
Normal file
@ -0,0 +1,654 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
using KlonFUN.Models;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Shared;
|
||||||
|
using Shared.Engine;
|
||||||
|
using Shared.Models;
|
||||||
|
using Shared.Models.Online.Settings;
|
||||||
|
|
||||||
|
namespace KlonFUN
|
||||||
|
{
|
||||||
|
public class KlonFUNInvoke
|
||||||
|
{
|
||||||
|
private static readonly Regex DirectFileRegex = new Regex(@"file\s*:\s*['""](?<url>https?://[^'"">\s]+\.m3u8[^'"">\s]*)['""]", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||||
|
private static readonly Regex YearRegex = new Regex(@"(19|20)\d{2}", RegexOptions.IgnoreCase);
|
||||||
|
private static readonly Regex NumberRegex = new Regex(@"(\d+)", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
private readonly OnlinesSettings _init;
|
||||||
|
private readonly IHybridCache _hybridCache;
|
||||||
|
private readonly Action<string> _onLog;
|
||||||
|
private readonly ProxyManager _proxyManager;
|
||||||
|
|
||||||
|
public KlonFUNInvoke(OnlinesSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||||
|
{
|
||||||
|
_init = init;
|
||||||
|
_hybridCache = hybridCache;
|
||||||
|
_onLog = onLog;
|
||||||
|
_proxyManager = proxyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SearchResult>> Search(string imdbId, string title, string originalTitle)
|
||||||
|
{
|
||||||
|
string cacheKey = $"KlonFUN:search:{imdbId}:{title}:{originalTitle}";
|
||||||
|
if (_hybridCache.TryGetValue(cacheKey, out List<SearchResult> cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(imdbId))
|
||||||
|
{
|
||||||
|
var byImdb = await SearchByQuery(imdbId);
|
||||||
|
if (byImdb?.Count > 0)
|
||||||
|
{
|
||||||
|
_hybridCache.Set(cacheKey, byImdb, cacheTime(20, init: _init));
|
||||||
|
_onLog?.Invoke($"KlonFUN: знайдено {byImdb.Count} результат(ів) за imdb_id={imdbId}");
|
||||||
|
return byImdb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var queries = new[] { originalTitle, title }
|
||||||
|
.Where(q => !string.IsNullOrWhiteSpace(q))
|
||||||
|
.Select(q => q.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var results = new List<SearchResult>();
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var query in queries)
|
||||||
|
{
|
||||||
|
var partial = await SearchByQuery(query);
|
||||||
|
if (partial == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var item in partial)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(item?.Url) && seen.Add(item.Url))
|
||||||
|
results.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.Count > 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.Count > 0)
|
||||||
|
{
|
||||||
|
_hybridCache.Set(cacheKey, results, cacheTime(20, init: _init));
|
||||||
|
_onLog?.Invoke($"KlonFUN: знайдено {results.Count} результат(ів) за назвою");
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog?.Invoke($"KlonFUN: помилка пошуку - {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KlonItem> GetItem(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string cacheKey = $"KlonFUN:item:{url}";
|
||||||
|
if (_hybridCache.TryGetValue(cacheKey, out KlonItem cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var headers = DefaultHeaders();
|
||||||
|
string html = await Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get());
|
||||||
|
if (string.IsNullOrWhiteSpace(html))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var doc = new HtmlDocument();
|
||||||
|
doc.LoadHtml(html);
|
||||||
|
|
||||||
|
string title = CleanText(doc.DocumentNode.SelectSingleNode("//h1[contains(@class,'seo-h1__position')]")?.InnerText);
|
||||||
|
|
||||||
|
string poster = doc.DocumentNode
|
||||||
|
.SelectSingleNode("//img[contains(@class,'cover-image')]")
|
||||||
|
?.GetAttributeValue("data-src", null);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(poster))
|
||||||
|
{
|
||||||
|
poster = doc.DocumentNode
|
||||||
|
.SelectSingleNode("//img[contains(@class,'cover-image')]")
|
||||||
|
?.GetAttributeValue("src", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
poster = NormalizeUrl(poster);
|
||||||
|
|
||||||
|
string playerUrl = doc.DocumentNode
|
||||||
|
.SelectSingleNode("//div[contains(@class,'film-player')]//iframe")
|
||||||
|
?.GetAttributeValue("data-src", null);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(playerUrl))
|
||||||
|
{
|
||||||
|
playerUrl = doc.DocumentNode
|
||||||
|
.SelectSingleNode("//div[contains(@class,'film-player')]//iframe")
|
||||||
|
?.GetAttributeValue("src", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
playerUrl = NormalizeUrl(playerUrl);
|
||||||
|
|
||||||
|
int year = 0;
|
||||||
|
var yearNode = doc.DocumentNode.SelectSingleNode("//div[contains(@class,'table__category') and contains(.,'Рік')]/following-sibling::div");
|
||||||
|
if (yearNode != null)
|
||||||
|
{
|
||||||
|
var yearMatch = YearRegex.Match(yearNode.InnerText ?? string.Empty);
|
||||||
|
if (yearMatch.Success)
|
||||||
|
int.TryParse(yearMatch.Value, out year);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new KlonItem
|
||||||
|
{
|
||||||
|
Url = url,
|
||||||
|
Title = title,
|
||||||
|
Poster = poster,
|
||||||
|
PlayerUrl = playerUrl,
|
||||||
|
IsSerialPlayer = IsSerialPlayer(playerUrl),
|
||||||
|
Year = year
|
||||||
|
};
|
||||||
|
|
||||||
|
_hybridCache.Set(cacheKey, result, cacheTime(40, init: _init));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog?.Invoke($"KlonFUN: помилка читання сторінки {url} - {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MovieStream>> GetMovieStreams(string playerUrl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(playerUrl))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string cacheKey = $"KlonFUN:movie:{playerUrl}";
|
||||||
|
if (_hybridCache.TryGetValue(cacheKey, out List<MovieStream> cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string playerHtml = await GetPlayerHtml(playerUrl);
|
||||||
|
if (string.IsNullOrWhiteSpace(playerHtml))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var streams = new List<MovieStream>();
|
||||||
|
|
||||||
|
JArray playerArray = ParsePlayerArray(playerHtml);
|
||||||
|
if (playerArray != null)
|
||||||
|
{
|
||||||
|
int index = 1;
|
||||||
|
foreach (JObject item in playerArray.OfType<JObject>())
|
||||||
|
{
|
||||||
|
string link = item.Value<string>("file");
|
||||||
|
if (string.IsNullOrWhiteSpace(link))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string voiceTitle = CleanText(item.Value<string>("title"));
|
||||||
|
if (string.IsNullOrWhiteSpace(voiceTitle))
|
||||||
|
voiceTitle = $"Варіант {index}";
|
||||||
|
|
||||||
|
streams.Add(new MovieStream
|
||||||
|
{
|
||||||
|
Title = voiceTitle,
|
||||||
|
Link = link
|
||||||
|
});
|
||||||
|
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streams.Count == 0)
|
||||||
|
{
|
||||||
|
var directMatch = DirectFileRegex.Match(playerHtml);
|
||||||
|
if (directMatch.Success)
|
||||||
|
{
|
||||||
|
streams.Add(new MovieStream
|
||||||
|
{
|
||||||
|
Title = "Основне джерело",
|
||||||
|
Link = directMatch.Groups["url"].Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streams.Count > 0)
|
||||||
|
{
|
||||||
|
_hybridCache.Set(cacheKey, streams, cacheTime(30, init: _init));
|
||||||
|
return streams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog?.Invoke($"KlonFUN: помилка парсингу плеєра фільму - {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SerialStructure> GetSerialStructure(string playerUrl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(playerUrl))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string cacheKey = $"KlonFUN:serial:{playerUrl}";
|
||||||
|
if (_hybridCache.TryGetValue(cacheKey, out SerialStructure cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string playerHtml = await GetPlayerHtml(playerUrl);
|
||||||
|
if (string.IsNullOrWhiteSpace(playerHtml))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
JArray playerArray = ParsePlayerArray(playerHtml);
|
||||||
|
if (playerArray == null || playerArray.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var structure = new SerialStructure();
|
||||||
|
var voiceCounter = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (JObject voiceObj in playerArray.OfType<JObject>())
|
||||||
|
{
|
||||||
|
var seasonsRaw = voiceObj["folder"] as JArray;
|
||||||
|
if (seasonsRaw == null || seasonsRaw.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string baseName = CleanText(voiceObj.Value<string>("title"));
|
||||||
|
if (string.IsNullOrWhiteSpace(baseName))
|
||||||
|
baseName = "Озвучення";
|
||||||
|
|
||||||
|
string displayName = BuildUniqueVoiceName(baseName, voiceCounter);
|
||||||
|
|
||||||
|
var voice = new SerialVoice
|
||||||
|
{
|
||||||
|
Key = displayName,
|
||||||
|
DisplayName = displayName,
|
||||||
|
Seasons = new Dictionary<int, List<SerialEpisode>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
int seasonFallback = 1;
|
||||||
|
foreach (JObject seasonObj in seasonsRaw.OfType<JObject>())
|
||||||
|
{
|
||||||
|
string seasonTitle = seasonObj.Value<string>("title");
|
||||||
|
int seasonNumber = ParseNumber(seasonTitle, seasonFallback);
|
||||||
|
|
||||||
|
var episodesRaw = seasonObj["folder"] as JArray;
|
||||||
|
if (episodesRaw == null || episodesRaw.Count == 0)
|
||||||
|
{
|
||||||
|
seasonFallback++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var episodes = new List<SerialEpisode>();
|
||||||
|
int episodeFallback = 1;
|
||||||
|
|
||||||
|
foreach (JObject episodeObj in episodesRaw.OfType<JObject>())
|
||||||
|
{
|
||||||
|
string link = episodeObj.Value<string>("file");
|
||||||
|
if (string.IsNullOrWhiteSpace(link))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string episodeTitle = CleanText(episodeObj.Value<string>("title"));
|
||||||
|
int episodeNumber = ParseNumber(episodeTitle, episodeFallback);
|
||||||
|
|
||||||
|
episodes.Add(new SerialEpisode
|
||||||
|
{
|
||||||
|
Number = episodeNumber,
|
||||||
|
Title = string.IsNullOrWhiteSpace(episodeTitle) ? $"Серія {episodeNumber}" : episodeTitle,
|
||||||
|
Link = link
|
||||||
|
});
|
||||||
|
|
||||||
|
episodeFallback++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (episodes.Count > 0)
|
||||||
|
voice.Seasons[seasonNumber] = episodes.OrderBy(e => e.Number).ToList();
|
||||||
|
|
||||||
|
seasonFallback++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (voice.Seasons.Count > 0)
|
||||||
|
structure.Voices.Add(voice);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structure.Voices.Count > 0)
|
||||||
|
{
|
||||||
|
structure.Voices = structure.Voices
|
||||||
|
.OrderBy(v => v.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_hybridCache.Set(cacheKey, structure, cacheTime(30, init: _init));
|
||||||
|
return structure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog?.Invoke($"KlonFUN: помилка парсингу структури серіалу - {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSerialPlayer(string playerUrl)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(playerUrl)
|
||||||
|
&& playerUrl.IndexOf("/serial/", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<SearchResult>> SearchByQuery(string query)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string cacheKey = $"KlonFUN:query:{query}";
|
||||||
|
if (_hybridCache.TryGetValue(cacheKey, out List<SearchResult> cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var headers = DefaultHeaders();
|
||||||
|
|
||||||
|
string form = $"do=search&subaction=search&story={HttpUtility.UrlEncode(query)}";
|
||||||
|
string html = await Http.Post(_init.cors(_init.host), form, headers: headers, proxy: _proxyManager.Get());
|
||||||
|
if (string.IsNullOrWhiteSpace(html))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var doc = new HtmlDocument();
|
||||||
|
doc.LoadHtml(html);
|
||||||
|
|
||||||
|
var results = new List<SearchResult>();
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var nodes = doc.DocumentNode.SelectNodes("//div[contains(@class,'short-news__slide-item')]");
|
||||||
|
if (nodes != null)
|
||||||
|
{
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
string href = node.SelectSingleNode(".//a[contains(@class,'short-news__small-card__link')]")?.GetAttributeValue("href", null)
|
||||||
|
?? node.SelectSingleNode(".//a[contains(@class,'card-link__style')]")?.GetAttributeValue("href", null);
|
||||||
|
|
||||||
|
href = NormalizeUrl(href);
|
||||||
|
if (string.IsNullOrWhiteSpace(href) || !seen.Add(href))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string title = CleanText(node.SelectSingleNode(".//div[contains(@class,'card-link__text')]")?.InnerText);
|
||||||
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
|
title = CleanText(node.SelectSingleNode(".//a[contains(@class,'card-link__style')]")?.InnerText);
|
||||||
|
|
||||||
|
string poster = node.SelectSingleNode(".//img[contains(@class,'card-poster__img')]")?.GetAttributeValue("data-src", null);
|
||||||
|
if (string.IsNullOrWhiteSpace(poster))
|
||||||
|
poster = node.SelectSingleNode(".//img[contains(@class,'card-poster__img')]")?.GetAttributeValue("src", null);
|
||||||
|
|
||||||
|
string meta = CleanText(node.SelectSingleNode(".//div[contains(@class,'subscribe-label-module')]")?.InnerText);
|
||||||
|
int year = 0;
|
||||||
|
if (!string.IsNullOrWhiteSpace(meta))
|
||||||
|
{
|
||||||
|
var yearMatch = YearRegex.Match(meta);
|
||||||
|
if (yearMatch.Success)
|
||||||
|
int.TryParse(yearMatch.Value, out year);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(title))
|
||||||
|
{
|
||||||
|
results.Add(new SearchResult
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Url = href,
|
||||||
|
Poster = NormalizeUrl(poster),
|
||||||
|
Year = year
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.Count == 0)
|
||||||
|
{
|
||||||
|
// Резервний парсер для спрощеної HTML-відповіді (наприклад, AJAX search).
|
||||||
|
var anchors = doc.DocumentNode.SelectNodes("//a[.//span[contains(@class,'searchheading')]]");
|
||||||
|
if (anchors != null)
|
||||||
|
{
|
||||||
|
foreach (var anchor in anchors)
|
||||||
|
{
|
||||||
|
string href = NormalizeUrl(anchor.GetAttributeValue("href", null));
|
||||||
|
if (string.IsNullOrWhiteSpace(href) || !seen.Add(href))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string title = CleanText(anchor.SelectSingleNode(".//span[contains(@class,'searchheading')]")?.InnerText);
|
||||||
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
results.Add(new SearchResult
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Url = href,
|
||||||
|
Poster = string.Empty,
|
||||||
|
Year = 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.Count > 0)
|
||||||
|
{
|
||||||
|
_hybridCache.Set(cacheKey, results, cacheTime(20, init: _init));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_onLog?.Invoke($"KlonFUN: помилка запиту пошуку '{query}' - {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetPlayerHtml(string playerUrl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(playerUrl))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string requestUrl = playerUrl;
|
||||||
|
if (ApnHelper.IsAshdiUrl(playerUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost))
|
||||||
|
requestUrl = ApnHelper.WrapUrl(_init, playerUrl);
|
||||||
|
|
||||||
|
var headers = DefaultHeaders();
|
||||||
|
return await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JArray ParsePlayerArray(string html)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(html))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string json = ExtractFileArray(html);
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
json = WebUtility.HtmlDecode(json).Replace("\\/", "/");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<JArray>(json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractFileArray(string html)
|
||||||
|
{
|
||||||
|
int searchIndex = 0;
|
||||||
|
while (searchIndex >= 0 && searchIndex < html.Length)
|
||||||
|
{
|
||||||
|
int fileIndex = html.IndexOf("file", searchIndex, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (fileIndex < 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int colonIndex = html.IndexOf(':', fileIndex);
|
||||||
|
if (colonIndex < 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int startIndex = colonIndex + 1;
|
||||||
|
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
|
||||||
|
startIndex++;
|
||||||
|
|
||||||
|
if (startIndex < html.Length && (html[startIndex] == '\'' || html[startIndex] == '"'))
|
||||||
|
{
|
||||||
|
startIndex++;
|
||||||
|
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
|
||||||
|
startIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIndex >= html.Length || html[startIndex] != '[')
|
||||||
|
{
|
||||||
|
searchIndex = fileIndex + 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int depth = 0;
|
||||||
|
bool inString = false;
|
||||||
|
bool escaped = false;
|
||||||
|
|
||||||
|
for (int i = startIndex; i < html.Length; i++)
|
||||||
|
{
|
||||||
|
char ch = html[i];
|
||||||
|
|
||||||
|
if (inString)
|
||||||
|
{
|
||||||
|
if (escaped)
|
||||||
|
{
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '\\')
|
||||||
|
{
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '"')
|
||||||
|
inString = false;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '"')
|
||||||
|
{
|
||||||
|
inString = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '[')
|
||||||
|
{
|
||||||
|
depth++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == ']')
|
||||||
|
{
|
||||||
|
depth--;
|
||||||
|
if (depth == 0)
|
||||||
|
return html.Substring(startIndex, i - startIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<HeadersModel> DefaultHeaders()
|
||||||
|
{
|
||||||
|
return new List<HeadersModel>
|
||||||
|
{
|
||||||
|
new HeadersModel("User-Agent", "Mozilla/5.0"),
|
||||||
|
new HeadersModel("Referer", _init.host)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeUrl(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
string value = WebUtility.HtmlDecode(url.Trim());
|
||||||
|
|
||||||
|
if (value.StartsWith("//"))
|
||||||
|
return "https:" + value;
|
||||||
|
|
||||||
|
if (value.StartsWith("/"))
|
||||||
|
return _init.host.TrimEnd('/') + value;
|
||||||
|
|
||||||
|
if (!value.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return _init.host.TrimEnd('/') + "/" + value.TrimStart('/');
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CleanText(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return HtmlEntity.DeEntitize(value).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ParseNumber(string value, int fallback)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
var match = NumberRegex.Match(value);
|
||||||
|
if (match.Success && int.TryParse(match.Value, out int parsed) && parsed > 0)
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildUniqueVoiceName(string baseName, Dictionary<string, int> voiceCounter)
|
||||||
|
{
|
||||||
|
if (!voiceCounter.TryGetValue(baseName, out int count))
|
||||||
|
{
|
||||||
|
voiceCounter[baseName] = 1;
|
||||||
|
return baseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
voiceCounter[baseName] = count;
|
||||||
|
return $"{baseName} #{count}";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = AppInit.conf.mikrotik
|
||||||
|
? mikrotik
|
||||||
|
: AppInit.conf.multiaccess
|
||||||
|
? init != null && init.cache_time > 0 ? init.cache_time : multiaccess
|
||||||
|
: home;
|
||||||
|
|
||||||
|
if (ctime > multiaccess)
|
||||||
|
ctime = multiaccess;
|
||||||
|
|
||||||
|
return TimeSpan.FromMinutes(ctime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
193
KlonFUN/ModInit.cs
Normal file
193
KlonFUN/ModInit.cs
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Shared;
|
||||||
|
using Shared.Engine;
|
||||||
|
using Shared.Models.Online.Settings;
|
||||||
|
using Shared.Models.Module;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
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 KlonFUN
|
||||||
|
{
|
||||||
|
public class ModInit
|
||||||
|
{
|
||||||
|
public static double Version => 1.0;
|
||||||
|
|
||||||
|
public static OnlinesSettings KlonFUN;
|
||||||
|
public static bool ApnHostProvided;
|
||||||
|
|
||||||
|
public static OnlinesSettings Settings
|
||||||
|
{
|
||||||
|
get => KlonFUN;
|
||||||
|
set => KlonFUN = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Модуль завантажено.
|
||||||
|
/// </summary>
|
||||||
|
public static void loaded(InitspaceModel initspace)
|
||||||
|
{
|
||||||
|
KlonFUN = new OnlinesSettings("KlonFUN", "https://klon.fun", streamproxy: false, useproxy: false)
|
||||||
|
{
|
||||||
|
displayname = "KlonFUN",
|
||||||
|
displayindex = 0,
|
||||||
|
proxy = new Shared.Models.Base.ProxySettings()
|
||||||
|
{
|
||||||
|
useAuth = true,
|
||||||
|
username = "",
|
||||||
|
password = "",
|
||||||
|
list = new string[] { "socks5://ip:port" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var conf = ModuleInvoke.Conf("KlonFUN", KlonFUN);
|
||||||
|
bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost);
|
||||||
|
conf.Remove("apn");
|
||||||
|
conf.Remove("apn_host");
|
||||||
|
KlonFUN = conf.ToObject<OnlinesSettings>();
|
||||||
|
if (hasApn)
|
||||||
|
ApnHelper.ApplyInitConf(apnEnabled, apnHost, KlonFUN);
|
||||||
|
ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost);
|
||||||
|
|
||||||
|
if (hasApn && apnEnabled)
|
||||||
|
{
|
||||||
|
KlonFUN.streamproxy = false;
|
||||||
|
}
|
||||||
|
else if (KlonFUN.streamproxy)
|
||||||
|
{
|
||||||
|
KlonFUN.apnstream = false;
|
||||||
|
KlonFUN.apn = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Додаємо підтримку "уточнити пошук".
|
||||||
|
AppInit.conf.online.with_search.Add("klonfun");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UpdateService
|
||||||
|
{
|
||||||
|
private static readonly string _connectUrl = "https://lmcuk.lampame.v6.rocks/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);
|
||||||
|
}
|
||||||
70
KlonFUN/Models/KlonFUNModels.cs
Normal file
70
KlonFUN/Models/KlonFUNModels.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace KlonFUN.Models
|
||||||
|
{
|
||||||
|
public class SearchResult
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Url { get; set; }
|
||||||
|
public string Poster { get; set; }
|
||||||
|
public int Year { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class KlonItem
|
||||||
|
{
|
||||||
|
public string Url { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Poster { get; set; }
|
||||||
|
public string PlayerUrl { get; set; }
|
||||||
|
public bool IsSerialPlayer { get; set; }
|
||||||
|
public int Year { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerVoice
|
||||||
|
{
|
||||||
|
public string title { get; set; }
|
||||||
|
public string file { get; set; }
|
||||||
|
public string subtitle { get; set; }
|
||||||
|
public string id { get; set; }
|
||||||
|
public List<PlayerSeason> folder { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerSeason
|
||||||
|
{
|
||||||
|
public string title { get; set; }
|
||||||
|
public List<PlayerEpisode> folder { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerEpisode
|
||||||
|
{
|
||||||
|
public string title { get; set; }
|
||||||
|
public string file { get; set; }
|
||||||
|
public string subtitle { get; set; }
|
||||||
|
public string id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MovieStream
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Link { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SerialEpisode
|
||||||
|
{
|
||||||
|
public int Number { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Link { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SerialVoice
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public Dictionary<int, List<SerialEpisode>> Seasons { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SerialStructure
|
||||||
|
{
|
||||||
|
public List<SerialVoice> Voices { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
40
KlonFUN/OnlineApi.cs
Normal file
40
KlonFUN/OnlineApi.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Shared.Models;
|
||||||
|
using Shared.Models.Base;
|
||||||
|
using Shared.Models.Module;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace KlonFUN
|
||||||
|
{
|
||||||
|
public class OnlineApi
|
||||||
|
{
|
||||||
|
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||||
|
HttpContext httpContext,
|
||||||
|
IMemoryCache memoryCache,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<(string name, string url, string plugin, int index)> 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<(string name, string url, string plugin, int index)>();
|
||||||
|
|
||||||
|
var init = ModInit.KlonFUN;
|
||||||
|
if (init.enable && !init.rip)
|
||||||
|
{
|
||||||
|
string url = init.overridehost;
|
||||||
|
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||||
|
url = $"{host}/klonfun";
|
||||||
|
|
||||||
|
online.Add((init.displayname, url, "klonfun", init.displayindex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return online;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
KlonFUN/manifest.json
Normal file
6
KlonFUN/manifest.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"enable": true,
|
||||||
|
"version": 3,
|
||||||
|
"initspace": "KlonFUN.ModInit",
|
||||||
|
"online": "KlonFUN.OnlineApi"
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@
|
|||||||
- [x] UATuTFun
|
- [x] UATuTFun
|
||||||
- [x] Makhno
|
- [x] Makhno
|
||||||
- [x] StarLight
|
- [x] StarLight
|
||||||
|
- [x] KlonFUN
|
||||||
|
|
||||||
### Anime and Dorama
|
### Anime and Dorama
|
||||||
- [x] AnimeON
|
- [x] AnimeON
|
||||||
@ -47,6 +48,7 @@ Create or update the module/repository.yaml file
|
|||||||
- Bamboo
|
- Bamboo
|
||||||
- Makhno
|
- Makhno
|
||||||
- StarLight
|
- StarLight
|
||||||
|
- KlonFUN
|
||||||
```
|
```
|
||||||
|
|
||||||
branch - optional, default main
|
branch - optional, default main
|
||||||
@ -90,6 +92,7 @@ Sources with APN support:
|
|||||||
- UaTUT
|
- UaTUT
|
||||||
- Mikai
|
- Mikai
|
||||||
- Makhno
|
- Makhno
|
||||||
|
- KlonFUN
|
||||||
|
|
||||||
## Source/player availability check script
|
## Source/player availability check script
|
||||||
|
|
||||||
|
|||||||
@ -87,6 +87,11 @@ namespace Uaflix.Controllers
|
|||||||
{
|
{
|
||||||
// Визначаємо URL для парсингу - або з параметра t, або з episode_url
|
// Визначаємо URL для парсингу - або з параметра t, або з episode_url
|
||||||
string urlToParse = !string.IsNullOrEmpty(t) ? t : Request.Query["episode_url"];
|
string urlToParse = !string.IsNullOrEmpty(t) ? t : Request.Query["episode_url"];
|
||||||
|
if (string.IsNullOrWhiteSpace(urlToParse))
|
||||||
|
{
|
||||||
|
OnLog("=== RETURN: play missing url OnError ===");
|
||||||
|
return OnError("uaflix", proxyManager);
|
||||||
|
}
|
||||||
|
|
||||||
var playResult = await invoke.ParseEpisode(urlToParse);
|
var playResult = await invoke.ParseEpisode(urlToParse);
|
||||||
if (playResult.streams != null && playResult.streams.Count > 0)
|
if (playResult.streams != null && playResult.streams.Count > 0)
|
||||||
@ -96,7 +101,7 @@ namespace Uaflix.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
OnLog("=== RETURN: play no streams ===");
|
OnLog("=== RETURN: play no streams ===");
|
||||||
return UpdateService.Validate(Content("Uaflix", "text/html; charset=utf-8"));
|
return OnError("uaflix", proxyManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Якщо є episode_url але немає play=true, це виклик для отримання інформації про стрім (для method: 'call')
|
// Якщо є episode_url але немає play=true, це виклик для отримання інформації про стрім (для method: 'call')
|
||||||
@ -114,14 +119,14 @@ namespace Uaflix.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
OnLog("=== RETURN: call method no streams ===");
|
OnLog("=== RETURN: call method no streams ===");
|
||||||
return UpdateService.Validate(Content("Uaflix", "text/html; charset=utf-8"));
|
return OnError("uaflix", proxyManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
string filmUrl = href;
|
string filmUrl = href;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(filmUrl))
|
if (string.IsNullOrEmpty(filmUrl))
|
||||||
{
|
{
|
||||||
var searchResults = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, title);
|
var searchResults = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial, original_language, source, title);
|
||||||
if (searchResults == null || searchResults.Count == 0)
|
if (searchResults == null || searchResults.Count == 0)
|
||||||
{
|
{
|
||||||
OnLog("No search results found");
|
OnLog("No search results found");
|
||||||
@ -129,21 +134,40 @@ namespace Uaflix.Controllers
|
|||||||
return OnError("uaflix", proxyManager);
|
return OnError("uaflix", proxyManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Для фільмів і серіалів показуємо вибір тільки якщо більше одного результату
|
var selectedResult = invoke.SelectBestSearchResult(searchResults, title, original_title, year);
|
||||||
if (searchResults.Count > 1)
|
if (selectedResult == null && searchResults.Count == 1)
|
||||||
|
selectedResult = searchResults[0];
|
||||||
|
|
||||||
|
if (selectedResult != null)
|
||||||
{
|
{
|
||||||
var similar_tpl = new SimilarTpl(searchResults.Count);
|
filmUrl = selectedResult.Url;
|
||||||
foreach (var res in searchResults)
|
OnLog($"Auto-selected best search result: {selectedResult.Url} (score={selectedResult.MatchScore}, year={selectedResult.Year})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var orderedResults = searchResults
|
||||||
|
.OrderByDescending(i => i.MatchScore)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var similar_tpl = new SimilarTpl(orderedResults.Count);
|
||||||
|
foreach (var res in orderedResults)
|
||||||
{
|
{
|
||||||
string link = $"{host}/uaflix?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)}";
|
string link = $"{host}/uaflix?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(), string.Empty, link, res.PosterUrl);
|
string y = res.Year > 0 ? res.Year.ToString() : string.Empty;
|
||||||
|
string details = res.Category switch
|
||||||
|
{
|
||||||
|
"films" => "Фільм",
|
||||||
|
"serials" => "Серіал",
|
||||||
|
"anime" => "Аніме",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
similar_tpl.Append(res.Title, y, details, link, res.PosterUrl);
|
||||||
}
|
}
|
||||||
OnLog($"=== RETURN: similar items ({searchResults.Count}) ===");
|
|
||||||
|
OnLog($"=== RETURN: similar items ({orderedResults.Count}) ===");
|
||||||
return rjson ? Content(similar_tpl.ToJson(), "application/json; charset=utf-8") : Content(similar_tpl.ToHtml(), "text/html; charset=utf-8");
|
return rjson ? Content(similar_tpl.ToJson(), "application/json; charset=utf-8") : Content(similar_tpl.ToHtml(), "text/html; charset=utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
filmUrl = searchResults[0].Url;
|
|
||||||
OnLog($"Auto-selected first search result: {filmUrl}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serial == 1)
|
if (serial == 1)
|
||||||
|
|||||||
@ -25,7 +25,7 @@ namespace Uaflix
|
|||||||
{
|
{
|
||||||
public class ModInit
|
public class ModInit
|
||||||
{
|
{
|
||||||
public static double Version => 3.6;
|
public static double Version => 3.7;
|
||||||
|
|
||||||
public static OnlinesSettings UaFlix;
|
public static OnlinesSettings UaFlix;
|
||||||
public static bool ApnHostProvided;
|
public static bool ApnHostProvided;
|
||||||
|
|||||||
@ -9,5 +9,10 @@ namespace Uaflix.Models
|
|||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
public int Year { get; set; }
|
public int Year { get; set; }
|
||||||
public string PosterUrl { get; set; }
|
public string PosterUrl { get; set; }
|
||||||
|
public string Category { get; set; }
|
||||||
|
public bool IsAnime { get; set; }
|
||||||
|
public int MatchScore { get; set; }
|
||||||
|
public bool TitleMatched { get; set; }
|
||||||
|
public bool YearMatched { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
1
check.sh
1
check.sh
@ -18,6 +18,7 @@ UaTUT|https://uk.uatut.fun/watch/2851|
|
|||||||
AnimeON|https://animeon.club/anime/924-provodzhalnicya-friren|https://animeon.club/api/player/47960/episode
|
AnimeON|https://animeon.club/anime/924-provodzhalnicya-friren|https://animeon.club/api/player/47960/episode
|
||||||
Bamboo|https://bambooua.com/dorama/938-18_again.html|
|
Bamboo|https://bambooua.com/dorama/938-18_again.html|
|
||||||
Mikai|https://mikai.me/anime/1272-friren-shcho-provodzhaie-v-ostanniu-put|https://api.mikai.me/v1/anime/1272
|
Mikai|https://mikai.me/anime/1272-friren-shcho-provodzhaie-v-ostanniu-put|https://api.mikai.me/v1/anime/1272
|
||||||
|
KlonFUN|https://klon.fun/filmy/3887-marsianyn-marsiianyn-rozshyrena-versiia.html|
|
||||||
SRC
|
SRC
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user