fix: lampac balancers (#10)

* Fix Kinobase

* Fix Videoseed

* Fix Vibix
This commit is contained in:
Alexander 2026-02-09 18:14:35 +05:00 committed by GitHub
parent 2d36e7aa6e
commit 659fb65256
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1024 additions and 67 deletions

View File

@ -139,7 +139,7 @@ namespace Online.Controllers
});
PlaywrightBase.GotoAsync(page, uri);
await browser.WaitForAnySelectorAsync(page, "#playerjsfile", ".uppod-media", ".alert").ConfigureAwait(false);
await browser.WaitForAnySelectorAsync(page, "#playerjsfile", "[id^='playerjsfile']", ".uppod-media", ".alert").ConfigureAwait(false);
string content = await page.ContentAsync().ConfigureAwait(false);

View File

@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using Shared.Models.Online.Vibix;
using Microsoft.Playwright;
using Shared.PlaywrightCore;
namespace Online.Controllers
{
@ -8,9 +10,28 @@ namespace Online.Controllers
{
public Vibix() : base(AppInit.conf.Vibix) { }
static readonly string[] qualityCandidates = new[] { "1080", "720", "480" };
StreamQualityTpl buildStreamQuality(string file)
{
var streams = new StreamQualityTpl();
if (string.IsNullOrEmpty(file))
return streams;
foreach (string q in qualityCandidates)
{
var g = new Regex($"{q}p?\\](\\{{[^\\}}]+\\}})?(?<file>https?://[^,\t\\[\\;\\{{ ]+)").Match(file).Groups;
if (!string.IsNullOrEmpty(g["file"].Value))
streams.Append(HostStreamProxy(g["file"].Value), $"{q}p");
}
return streams;
}
[HttpGet]
[Route("lite/vibix")]
async public Task<ActionResult> Index(string imdb_id, long kinopoisk_id, string title, string original_title, int s = -1, bool rjson = false)
async public Task<ActionResult> Index(string imdb_id, long kinopoisk_id, string title, string original_title, int s = -1, bool rjson = false, int voiceover = 0)
{
if (await IsRequestBlocked(rch: true))
return badInitMsg;
@ -18,20 +39,142 @@ namespace Online.Controllers
if (string.IsNullOrEmpty(init.token))
return OnError();
string enc_title = HttpUtility.UrlEncode(title);
string enc_original_title = HttpUtility.UrlEncode(original_title);
if (!hybridCache.TryGetValue($"vibix:v2:view:{kinopoisk_id}:{imdb_id}", out (Dictionary<string, JObject> seasons, string iframe) v2Cache))
{
v2Cache = await searchV2(imdb_id, kinopoisk_id);
if (v2Cache.seasons != null || !string.IsNullOrEmpty(v2Cache.iframe))
hybridCache.Set($"vibix:v2:view:{kinopoisk_id}:{imdb_id}", v2Cache, cacheTime(40));
}
if (v2Cache.seasons != null)
{
if (PlaywrightBrowser.Status == PlaywrightStatus.disabled)
return OnError();
#region Сериал
if (s == -1)
{
var tpl = new SeasonTpl(v2Cache.seasons.Count);
foreach (var season in v2Cache.seasons)
{
string link = $"{host}/lite/vibix?rjson={rjson}&kinopoisk_id={kinopoisk_id}&imdb_id={imdb_id}&title={enc_title}&original_title={enc_original_title}&s={season.Key}";
tpl.Append($"{season.Key} сезон", link, season.Key);
}
return await ContentTpl(tpl);
}
else
{
string sArhc = s.ToString();
var videos = v2Cache.seasons.First(i => i.Key == sArhc).Value["videos"].ToObject<Dictionary<string, JObject>>();
var etpl = new EpisodeTpl(videos.Count);
foreach (var video in videos)
{
string iframe = addIframeArgs(video.Value.Value<string>("iframe"));
etpl.Append($"{video.Key} серия", title ?? original_title, sArhc, video.Key, accsArgs($"{host}/lite/vibix/video/{AesTo.Encrypt(buildVideoPayload(iframe, null))}"), "call", vast: init.vast);
}
return await ContentTpl(etpl);
}
#endregion
}
var data = await search(imdb_id, kinopoisk_id);
if (data == null)
return OnError();
if (data.type == "serial" && PlaywrightBrowser.Status != PlaywrightStatus.disabled && !string.IsNullOrEmpty(data.embed_code))
{
var serials = await getSerials(imdb_id, kinopoisk_id);
if (serials?.seasons == null)
return OnError();
int defaultVoiceover = data.voiceovers?.FirstOrDefault()?.id ?? 0;
int activeVoiceover = voiceover > 0 ? voiceover : defaultVoiceover;
VoiceTpl? vtpl = null;
if (data.voiceovers != null && data.voiceovers.Length > 0)
{
var voices = new VoiceTpl(data.voiceovers.Length);
foreach (var voice in data.voiceovers)
{
string link = $"{host}/lite/vibix?rjson={rjson}&kinopoisk_id={kinopoisk_id}&imdb_id={imdb_id}&title={enc_title}&original_title={enc_original_title}&s={s}&voiceover={voice.id}";
voices.Append(voice.name, voice.id == activeVoiceover, link);
}
vtpl = voices;
}
if (s == -1)
{
var tpl = new SeasonTpl(serials.seasons.Length);
for (int i = 0; i < serials.seasons.Length; i++)
{
int seasonNumber = i + 1;
string link = $"{host}/lite/vibix?rjson={rjson}&kinopoisk_id={kinopoisk_id}&imdb_id={imdb_id}&title={enc_title}&original_title={enc_original_title}&s={seasonNumber}&voiceover={activeVoiceover}";
tpl.Append($"{seasonNumber} сезон", link, seasonNumber);
}
return await ContentTpl(tpl);
}
else
{
int seasonIndex = s - 1;
if (seasonIndex < 0 || seasonIndex >= serials.seasons.Length)
return OnError();
var season = serials.seasons[seasonIndex];
var etpl = new EpisodeTpl(vtpl, season.series?.Length ?? 0);
if (season.series != null)
{
foreach (var episode in season.series)
{
string embed = applyEmbedArgs(data.embed_code, s.ToString(), episode.id.ToString(), activeVoiceover);
etpl.Append(episode.name ?? $"{episode.id} серия", title ?? original_title, s.ToString(), episode.id.ToString(), accsArgs($"{host}/lite/vibix/video/{AesTo.Encrypt(buildVideoPayload(null, embed))}"), "call", vast: init.vast);
}
}
return await ContentTpl(etpl);
}
}
if (data.type == "movie" && PlaywrightBrowser.Status != PlaywrightStatus.disabled && !string.IsNullOrEmpty(data.iframe_url))
{
var mtpl = new MovieTpl(title, original_title, 1);
string iframe = addIframeArgs(data.iframe_url);
if (data.voiceovers != null && data.voiceovers.Length > 0)
{
int defaultVoiceover = data.voiceovers.FirstOrDefault()?.id ?? 0;
int activeVoiceover = voiceover > 0 ? voiceover : defaultVoiceover;
foreach (var voice in data.voiceovers)
{
string embed = applyEmbedArgs(data.embed_code, null, null, voice.id);
mtpl.Append(voice.name, accsArgs($"{host}/lite/vibix/video/{AesTo.Encrypt(buildVideoPayload(iframe, embed))}") + "#.m3u8", "call", voice_name: voice.name, vast: init.vast);
}
}
else
{
mtpl.Append("По-умолчанию", accsArgs($"{host}/lite/vibix/video/{AesTo.Encrypt(buildVideoPayload(iframe, data.embed_code))}") + "#.m3u8", "call", vast: init.vast);
}
return await ContentTpl(mtpl);
}
rhubFallback:
var cache = await InvokeCacheResult<EmbedModel>(ipkey($"vibix:iframe:{data.iframe_url}"), 20, async e =>
{
string api_url = data.iframe_url
string domain = getFrontendDomain();
string api_url = data.iframe_url
.Replace("/embed/", "/api/v1/embed/")
.Replace("/embed-serials/", "/api/v1/embed-serials/");
api_url += $"?iframe_url={HttpUtility.UrlEncode(data.iframe_url)}";
api_url += $"&kp={CrypTo.unic(6).ToLower()}";
api_url += "&domain=cm.vibix.biz&parent_domain=cm.vibix.biz";
api_url += $"&domain={domain}&parent_domain={domain}";
var api_headers = HeadersModel.Init(
("accept", "*/*"),
@ -56,21 +199,13 @@ namespace Online.Controllers
if (data.type == "movie")
{
#region Фильм
return await ContentTpl(cache, () =>
return await ContentTpl(cache, () =>
{
var mtpl = new MovieTpl(title, original_title, 1);
foreach (var movie in cache.Value.playlist)
{
var streams = new StreamQualityTpl();
foreach (string q in new string[] { "1080", "720", "480" })
{
var g = new Regex($"{q}p?\\](\\{{[^\\}}]+\\}})?(?<file>https?://[^,\t\\[\\;\\{{ ]+)").Match(movie.file).Groups;
if (!string.IsNullOrEmpty(g["file"].Value))
streams.Append(HostStreamProxy(g["file"].Value), $"{q}p");
}
var streams = buildStreamQuality(movie.file);
mtpl.Append(movie.title, streams.Firts().link, streamquality: streams, vast: init.vast);
}
@ -84,9 +219,6 @@ namespace Online.Controllers
#region Сериал
return await ContentTpl(cache, () =>
{
string enc_title = HttpUtility.UrlEncode(title);
string enc_original_title = HttpUtility.UrlEncode(original_title);
if (s == -1)
{
var tpl = new SeasonTpl(cache.Value.playlist.Length);
@ -121,14 +253,7 @@ namespace Online.Controllers
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(file))
continue;
var streams = new StreamQualityTpl();
foreach (string q in new string[] { "1080", "720", "480" })
{
var g = new Regex($"{q}p?\\](\\{{[^\\}}]+\\}})?(?<file>https?://[^,\t\\[\\;\\{{ ]+)").Match(file).Groups;
if (!string.IsNullOrEmpty(g["file"].Value))
streams.Append(HostStreamProxy(g["file"].Value), $"{q}p");
}
var streams = buildStreamQuality(file);
etpl.Append(name, title ?? original_title, sArhc, Regex.Match(name, "([0-9]+)").Groups[1].Value, streams.Firts().link, streamquality: streams, vast: init.vast);
}
@ -140,7 +265,440 @@ namespace Online.Controllers
#endregion
}
}
string addIframeArgs(string iframe)
{
if (string.IsNullOrEmpty(iframe))
return iframe;
if (iframe.Contains("domain=") || iframe.Contains("parent_domain="))
return iframe;
string delimiter = iframe.Contains("?") ? "&" : "?";
string domain = getFrontendDomain();
return $"{iframe}{delimiter}domain={domain}&parent_domain={domain}";
}
string buildVideoPayload(string iframe, string embed)
{
if (string.IsNullOrEmpty(embed))
return iframe;
return JObject.FromObject(new
{
iframe,
embed
}).ToString();
}
string buildEmbedHtml(string embedCode)
{
if (string.IsNullOrEmpty(embedCode))
return null;
embedCode = normalizeEmbedCode(embedCode);
return $@"<!doctype html>
<html>
<head>
<meta name=""viewport"" content=""width=device-width, initial-scale=1"">
<script src=""https://graphicslab.io/sdk/v2/rendex-sdk.min.js""></script>
</head>
<body>
{embedCode}
</body>
</html>";
}
string applyEmbedArgs(string embedCode, string season, string episodes, int voiceover)
{
if (string.IsNullOrEmpty(embedCode))
return embedCode;
embedCode = normalizeEmbedCode(embedCode);
var attrs = new List<string>();
if (!string.IsNullOrEmpty(season) && !embedCode.Contains("data-season="))
attrs.Add($"data-season=\"{season}\"");
if (!string.IsNullOrEmpty(episodes) && !embedCode.Contains("data-episodes="))
attrs.Add($"data-episodes=\"{episodes}\"");
if (voiceover > 0 && !embedCode.Contains("data-voiceover="))
attrs.Add($"data-voiceover=\"{voiceover}\"");
if (voiceover > 0 && !embedCode.Contains("data-voiceover-only="))
attrs.Add("data-voiceover-only=\"true\"");
if (attrs.Count == 0)
return embedCode;
string insert = $"<ins {string.Join(" ", attrs)} ";
return Regex.Replace(embedCode, "<ins\\b", insert, RegexOptions.IgnoreCase);
}
string normalizeEmbedCode(string embedCode)
{
if (string.IsNullOrEmpty(embedCode))
return embedCode;
if (Regex.IsMatch(embedCode, "<ins\\b", RegexOptions.IgnoreCase))
return embedCode;
return $"<ins {embedCode}></ins>";
}
#region Video
[HttpGet]
[Route("lite/vibix/video/{*iframe}")]
async public ValueTask<ActionResult> Video(string iframe)
{
if (PlaywrightBrowser.Status == PlaywrightStatus.disabled)
return OnError();
if (await IsRequestBlocked(rch: true))
return badInitMsg;
string decrypted = AesTo.Decrypt(iframe);
if (string.IsNullOrEmpty(decrypted))
return OnError();
string embedCode = null;
string iframeUrl = decrypted;
if (decrypted.StartsWith("{"))
{
try
{
var payload = JObject.Parse(decrypted);
iframeUrl = payload.Value<string>("iframe");
embedCode = payload.Value<string>("embed");
}
catch { }
}
iframeUrl = addIframeArgs(iframeUrl);
if (string.IsNullOrEmpty(iframeUrl) && string.IsNullOrEmpty(embedCode))
return OnError();
string cacheKey = iframeUrl ?? CrypTo.md5(embedCode ?? string.Empty);
return await InvkSemaphore($"vibix:video:{cacheKey}:{proxyManager?.CurrentProxyIp}", async key =>
{
if (!hybridCache.TryGetValue(key, out (string location, StreamQualityTpl streamquality) cache))
{
int bestQuality = 0;
var streamquality = new StreamQualityTpl();
string referer = Regex.Match(iframeUrl ?? string.Empty, "(^https?://[^/]+)").Groups[1].Value;
if (string.IsNullOrEmpty(referer))
referer = $"https://{getFrontendDomain()}";
var headers = httpHeaders(init, HeadersModel.Init
(
("referer", referer)
));
TimeSpan? cacheTtl = null;
try
{
using (var browser = new PlaywrightBrowser(init.priorityBrowser))
{
var page = await browser.NewPageAsync(init.plugin, proxy: proxy_data, headers: headers?.ToDictionary()).ConfigureAwait(false);
if (page == null)
return OnError();
await page.AddInitScriptAsync(@"() => {
try {
localStorage.setItem('pljsquality', '1080p');
localStorage.setItem('ksquality', '1080p');
} catch (e) {}
}").ConfigureAwait(false);
await page.RouteAsync("**/api/v1/embed/**", async route =>
{
try
{
if (route.Request.Method != "GET")
{
await route.ContinueAsync();
return;
}
await route.ContinueAsync();
}
catch
{
await route.ContinueAsync();
}
});
await page.RouteAsync("**/*", async route =>
{
try
{
if (!string.IsNullOrEmpty(cache.location))
{
await route.AbortAsync();
return;
}
var reqUrl = route.Request.Url;
if (reqUrl.Contains("/hls/", StringComparison.OrdinalIgnoreCase) &&
reqUrl.Contains("/seg-", StringComparison.OrdinalIgnoreCase) &&
reqUrl.Contains(".ts", StringComparison.OrdinalIgnoreCase))
{
var playlist = Regex.Replace(reqUrl, @"/seg-[^/?]+\.ts", "/index.m3u8", RegexOptions.IgnoreCase);
updateCacheTtl(ref cacheTtl, reqUrl);
updateLocation(ref cache.location, ref bestQuality, playlist, streamquality);
}
if (reqUrl.IndexOf(".m3u8", StringComparison.OrdinalIgnoreCase) >= 0 ||
(reqUrl.IndexOf(".mp4", StringComparison.OrdinalIgnoreCase) >= 0 &&
reqUrl.IndexOf(".ts", StringComparison.OrdinalIgnoreCase) < 0))
{
updateCacheTtl(ref cacheTtl, reqUrl);
updateLocation(ref cache.location, ref bestQuality, reqUrl, streamquality);
}
if (await PlaywrightBase.AbortOrCache(page, route, abortMedia: true, fullCacheJS: true))
return;
await route.ContinueAsync();
}
catch { }
});
var options = new PageGotoOptions()
{
Timeout = 15_000,
WaitUntil = WaitUntilState.NetworkIdle
};
IResponse result = null;
try
{
await page.GotoAsync("https://cm.vibix.biz", new PageGotoOptions()
{
Timeout = 10_000,
WaitUntil = WaitUntilState.DOMContentLoaded
}).ConfigureAwait(false);
}
catch { }
if (!string.IsNullOrEmpty(embedCode))
{
try
{
await page.SetContentAsync(buildEmbedHtml(embedCode), new PageSetContentOptions()
{
Timeout = 15_000,
WaitUntil = WaitUntilState.NetworkIdle
}).ConfigureAwait(false);
}
catch { }
}
else if (!string.IsNullOrEmpty(iframeUrl))
{
try
{
await page.SetContentAsync(PlaywrightBase.IframeHtml(iframeUrl), new PageSetContentOptions()
{
Timeout = 15_000,
WaitUntil = WaitUntilState.NetworkIdle
}).ConfigureAwait(false);
}
catch { }
}
if (string.IsNullOrEmpty(cache.location) && !string.IsNullOrEmpty(iframeUrl))
result = await page.GotoAsync(iframeUrl, options).ConfigureAwait(false);
if (result != null)
{
try
{
await page.EvaluateAsync(@"() => {
const video = document.querySelector('video');
if (video) {
video.muted = true;
video.play().catch(() => {});
}
const btn = document.querySelector('.vjs-big-play-button, .plyr__control--overlaid, .jw-icon-playback, .jw-icon-play');
if (btn) btn.click();
}").ConfigureAwait(false);
}
catch { }
}
if (string.IsNullOrEmpty(cache.location))
await page.WaitForTimeoutAsync(3000).ConfigureAwait(false);
if (result != null && string.IsNullOrEmpty(cache.location))
{
string html = await page.ContentAsync().ConfigureAwait(false);
cache.location = Regex.Match(html, "<video preload=\"none\" src=\"(https?://[^\"]+)\"").Groups[1].Value;
if (!cache.location.Contains(".m3u") && !cache.location.Contains(".mp4"))
cache.location = null;
}
PlaywrightBase.WebLog("SET", iframe, cache.location, proxy_data);
}
if (string.IsNullOrEmpty(cache.location))
{
proxyManager?.Refresh();
return OnError();
}
}
catch
{
return OnError();
}
proxyManager?.Success();
cache.streamquality = streamquality;
hybridCache.Set(key, cache, resolveCacheTtl(cacheTtl, cacheTime(20)));
}
string refererStream = Regex.Match(iframeUrl ?? string.Empty, "(^https?://[^/]+)").Groups[1].Value;
if (string.IsNullOrEmpty(refererStream))
refererStream = "https://cm.vibix.biz";
var headers_stream = httpHeaders(init.corsHost(), HeadersModel.Join(HeadersModel.Init("referer", refererStream), init.headers_stream));
string link = HostStreamProxy(cache.location, headers: headers_stream);
var streamQuality = cache.streamquality;
bool hasQualities = false;
try
{
if (streamQuality.Any())
hasQualities = true;
}
catch { }
if (!hasQualities)
return ContentTo(VideoTpl.ToJson("play", link, "auto", vast: init.vast));
return ContentTo(VideoTpl.ToJson("play", streamQuality.Firts().link, "auto", streamquality: streamQuality, vast: init.vast));
});
}
#endregion
void updateLocation(ref string location, ref int bestQuality, string candidate, StreamQualityTpl streamquality)
{
if (string.IsNullOrEmpty(candidate))
return;
int q = getQualityFromUrl(candidate);
if (q == 0)
{
if (tryExpandQualityVariants(candidate, streamquality, ref bestQuality, ref location))
return;
if (string.IsNullOrEmpty(location))
location = candidate;
return;
}
streamquality.Append(HostStreamProxy(candidate), $"{q}p");
if (q > bestQuality)
{
bestQuality = q;
location = candidate;
}
}
bool tryExpandQualityVariants(string candidate, StreamQualityTpl streamquality, ref int bestQuality, ref string location)
{
var match = Regex.Match(candidate, "/hls/\\d+/\\d+/(?<id>\\d+)\\.mp4/index\\.m3u8", RegexOptions.IgnoreCase);
if (!match.Success)
return false;
string id = match.Groups["id"].Value;
int endIdx = candidate.IndexOf($"{id}.mp4/index.m3u8", StringComparison.OrdinalIgnoreCase);
if (endIdx < 0)
return false;
string prefix = candidate.Substring(0, endIdx);
string suffix = candidate.Substring(endIdx + $"{id}.mp4/index.m3u8".Length);
foreach (int quality in new[] { 1080, 720, 480 })
{
string variant = $"{prefix}{id}_{quality}p.mp4/index.m3u8{suffix}";
streamquality.Append(HostStreamProxy(variant), $"{quality}p");
if (quality > bestQuality)
{
bestQuality = quality;
location = variant;
}
}
return true;
}
int getQualityFromUrl(string url)
{
if (string.IsNullOrEmpty(url))
return 0;
var match = Regex.Match(url, "(2160|1440|1080|720|480|360|240)p", RegexOptions.IgnoreCase);
if (match.Success && int.TryParse(match.Groups[1].Value, out int q))
return q;
return 0;
}
void updateCacheTtl(ref TimeSpan? cacheTtl, string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return;
var query = HttpUtility.ParseQueryString(uri.Query);
if (!long.TryParse(query["expires"], out long expires))
return;
DateTimeOffset expiresAt;
try
{
expiresAt = expires > 9999999999 ? DateTimeOffset.FromUnixTimeMilliseconds(expires) : DateTimeOffset.FromUnixTimeSeconds(expires);
}
catch
{
return;
}
var ttl = expiresAt - DateTimeOffset.UtcNow - TimeSpan.FromSeconds(45);
if (ttl <= TimeSpan.Zero)
return;
if (ttl < TimeSpan.FromSeconds(30))
ttl = TimeSpan.FromSeconds(30);
if (!cacheTtl.HasValue || ttl < cacheTtl.Value)
cacheTtl = ttl;
}
TimeSpan resolveCacheTtl(TimeSpan? cacheTtl, TimeSpan fallback)
{
if (!cacheTtl.HasValue)
return fallback;
if (cacheTtl.Value <= TimeSpan.Zero)
return TimeSpan.FromSeconds(1);
return cacheTtl.Value < fallback ? cacheTtl.Value : fallback;
}
string getFrontendDomain()
{
if (Uri.TryCreate(host, UriKind.Absolute, out var uri) && !string.IsNullOrEmpty(uri.Host))
return uri.Host;
if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var fallback) && !string.IsNullOrEmpty(fallback.Host))
return fallback.Host;
return "cm.vibix.biz";
}
#region search
async ValueTask<Video> search(string imdb_id, long kinopoisk_id)
@ -179,11 +737,111 @@ namespace Online.Controllers
return null;
}
if (string.IsNullOrEmpty(video.iframe_url) || string.IsNullOrEmpty(video.type))
if (string.IsNullOrEmpty(video.type))
return null;
if (string.IsNullOrEmpty(video.iframe_url) || !video.iframe_url.Contains("token="))
{
string iframe = getIframeFromEmbed(video.embed_code);
if (!string.IsNullOrEmpty(iframe))
video.iframe_url = iframe;
}
if (string.IsNullOrEmpty(video.iframe_url))
return null;
return video;
}
#endregion
#region serials
async Task<SerialsRoot> getSerials(string imdb_id, long kinopoisk_id)
{
if (string.IsNullOrEmpty(imdb_id) && kinopoisk_id == 0)
return null;
string uri = kinopoisk_id > 0 ? $"kp/{kinopoisk_id}" : $"imdb/{imdb_id}";
var serials = await httpHydra.Get<SerialsRoot>($"{init.host}/api/v1/serials/{uri}", safety: true, addheaders: HeadersModel.Init(
("Accept", "application/json"),
("Authorization", $"Bearer {init.token}"),
("X-CSRF-TOKEN", "")
));
if (serials?.seasons == null)
{
proxyManager?.Refresh();
return null;
}
proxyManager?.Success();
return serials;
}
#endregion
string getIframeFromEmbed(string embed)
{
if (string.IsNullOrEmpty(embed))
return null;
string iframe = Regex.Match(embed, "src=[\"'](?<url>https?://[^\"']+)", RegexOptions.IgnoreCase).Groups["url"].Value;
if (!string.IsNullOrEmpty(iframe))
return iframe;
return null;
}
#region searchV2
async Task<(Dictionary<string, JObject> seasons, string iframe)> searchV2(string imdb_id, long kinopoisk_id)
{
string imdbArg = !string.IsNullOrEmpty(imdb_id) ? $"&imdb={imdb_id}" : null;
string kpArg = kinopoisk_id > 0 ? $"&kp={kinopoisk_id}" : null;
JToken data = null;
foreach (var candidate in new (string item, string arg)[] { ("movie", kpArg), ("movie", imdbArg), ("serial", kpArg), ("serial", imdbArg) })
{
data = await goSearchV2(candidate.item, candidate.arg);
if (data != null)
break;
}
if (data == null)
return (null, null);
string iframe = data.Value<string>("iframe");
var seasons = data["seasons"]?.ToObject<Dictionary<string, JObject>>();
if (string.IsNullOrEmpty(iframe) && seasons == null)
return (null, null);
proxyManager?.Success();
return (seasons, iframe);
}
async Task<JToken> goSearchV2(string item, string arg)
{
if (string.IsNullOrEmpty(arg))
return null;
long kp = 0;
string imdb = null;
if (arg.StartsWith("&kp=", StringComparison.OrdinalIgnoreCase))
long.TryParse(arg[4..], out kp);
else if (arg.StartsWith("&imdb=", StringComparison.OrdinalIgnoreCase))
imdb = arg[6..];
var video = await goSearch(imdb, kp);
if (video == null || !string.Equals(video.type, item, StringComparison.OrdinalIgnoreCase))
return null;
if (string.IsNullOrEmpty(video.iframe_url))
return null;
return new JObject
{
["iframe"] = video.iframe_url
};
}
#endregion
}
}

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Playwright;
using System.Linq;
using Newtonsoft.Json.Linq;
using Shared.PlaywrightCore;
@ -9,10 +10,23 @@ namespace Online.Controllers
{
public Videoseed() : base(AppInit.conf.Videoseed) { }
static int ExtractOrderNumber(string key)
{
string numericKey = Regex.Replace(key ?? string.Empty, "\\D", string.Empty);
return int.TryParse(numericKey, out int n) ? n : int.MaxValue;
}
string GetEscapedToken()
{
if (string.IsNullOrEmpty(init.token))
return null;
return Uri.EscapeDataString(NormalizeToken(init.token));
}
[HttpGet]
[Route("lite/videoseed")]
async public Task<ActionResult> Index(string imdb_id, long kinopoisk_id, string title, string original_title, int year, int s = -1, bool rjson = false, int serial = -1)
{
async public Task<ActionResult> Index(string imdb_id, long kinopoisk_id, string title, string original_title, int year, int s = -1, bool rjson = false, int serial = -1, string t = null) {
if (PlaywrightBrowser.Status == PlaywrightStatus.disabled)
return OnError();
@ -25,22 +39,40 @@ namespace Online.Controllers
return await InvkSemaphore($"videoseed:view:{kinopoisk_id}:{imdb_id}:{original_title}", async key =>
{
#region search
if (!hybridCache.TryGetValue(key, out (Dictionary<string, JObject> seasons, string iframe) cache))
if (!hybridCache.TryGetValue(key, out (Dictionary<string, JObject> seasons, string iframe, Dictionary<string, string> translations) cache))
{
var data = await goSearch(serial, kinopoisk_id > 0, $"&kp={kinopoisk_id}")
?? await goSearch(serial, !string.IsNullOrEmpty(imdb_id), $"&tmdb={imdb_id}")
?? await goSearch(serial, !string.IsNullOrEmpty(original_title), $"&q={HttpUtility.UrlEncode(original_title)}&release_year_from={year - 1}&release_year_to={year + 1}");
var candidates = new (bool ok, string arg)[]
{
(kinopoisk_id > 0, $"&kp={kinopoisk_id}"),
(!string.IsNullOrEmpty(imdb_id), $"&imdb={imdb_id}"),
(!string.IsNullOrEmpty(imdb_id), $"&imdb_id={imdb_id}"),
(!string.IsNullOrEmpty(imdb_id), $"&tmdb={imdb_id}"),
(!string.IsNullOrEmpty(original_title), $"&q={HttpUtility.UrlEncode(original_title)}&release_year_from={year - 1}&release_year_to={year + 1}")
};
JToken data = null;
foreach (var c in candidates)
{
data = await goSearch(serial, c.ok, c.arg);
if (data != null)
break;
}
if (data == null)
{
proxyManager?.Refresh();
return OnError();
}
cache.translations = data?["translation_iframe"]?.ToObject<Dictionary<string, JObject>>()
?.ToDictionary(item => item.Key, item => item.Value?.Value<string>("iframe"));
if (serial == 1)
cache.seasons = data?["seasons"]?.ToObject<Dictionary<string, JObject>>();
else
{
cache.iframe = data?.Value<string>("iframe");
}
if (cache.seasons == null && string.IsNullOrEmpty(cache.iframe))
{
@ -58,6 +90,17 @@ namespace Online.Controllers
#region Фильм
var mtpl = new MovieTpl(title, original_title, 1);
mtpl.Append("По-умолчанию", accsArgs($"{host}/lite/videoseed/video/{AesTo.Encrypt(cache.iframe)}") + "#.m3u8", "call", vast: init.vast);
if (cache.translations != null)
{
foreach (var translation in cache.translations)
{
if (string.IsNullOrEmpty(translation.Value) || translation.Value == cache.iframe)
continue;
mtpl.Append(translation.Key, accsArgs($"{host}/lite/videoseed/video/{AesTo.Encrypt(translation.Value)}") + "#.m3u8", "call", voice_name: translation.Key, vast: init.vast);
}
}
return await ContentTpl(mtpl);
#endregion
@ -72,7 +115,9 @@ namespace Online.Controllers
{
var tpl = new SeasonTpl(cache.seasons.Count);
foreach (var season in cache.seasons)
foreach (var season in cache.seasons
.OrderBy(item => ExtractOrderNumber(item.Key))
.ThenBy(item => item.Key))
{
string link = $"{host}/lite/videoseed?rjson={rjson}&kinopoisk_id={kinopoisk_id}&imdb_id={imdb_id}&title={enc_title}&original_title={enc_original_title}&s={season.Key}";
tpl.Append($"{season.Key} сезон", link, season.Key);
@ -83,14 +128,42 @@ namespace Online.Controllers
else
{
string sArhc = s.ToString();
string activeTranslation = t;
bool hasTranslations = false;
VoiceTpl vtpl = default;
if (cache.translations?.Count > 0)
{
vtpl = new VoiceTpl(cache.translations.Count);
hasTranslations = true;
foreach (var translation in cache.translations)
{
if (string.IsNullOrWhiteSpace(activeTranslation))
activeTranslation = translation.Key;
string link = $"{host}/lite/videoseed?rjson={rjson}&kinopoisk_id={kinopoisk_id}&imdb_id={imdb_id}&title={enc_title}&original_title={enc_original_title}&s={s}&t={HttpUtility.UrlEncode(translation.Key)}";
vtpl.Append(translation.Key, activeTranslation == translation.Key, link);
}
}
var videos = cache.seasons.First(i => i.Key == sArhc).Value["videos"].ToObject<Dictionary<string, JObject>>();
var etpl = new EpisodeTpl(videos.Count);
var etpl = hasTranslations ? new EpisodeTpl(vtpl, videos.Count) : new EpisodeTpl(videos.Count);
foreach (var video in videos)
string defaultAudio = null;
if (!string.IsNullOrWhiteSpace(activeTranslation) && cache.translations != null && cache.translations.TryGetValue(activeTranslation, out string translationIframe))
defaultAudio = ExtractDefaultAudio(translationIframe);
foreach (var video in videos
.OrderBy(item => ExtractOrderNumber(item.Key))
.ThenBy(item => item.Key))
{
string iframe = video.Value.Value<string>("iframe");
etpl.Append($"{video.Key} серия", title ?? original_title, sArhc, video.Key, accsArgs($"{host}/lite/videoseed/video/{AesTo.Encrypt(iframe)}"), "call", vast: init.vast);
if (!string.IsNullOrEmpty(defaultAudio))
iframe = ApplyDefaultAudio(iframe, defaultAudio);
etpl.Append($"{video.Key} серия", title ?? original_title, sArhc, video.Key, accsArgs($"{host}/lite/videoseed/video/{AesTo.Encrypt(iframe)}"), "call", vast: init.vast);
}
return await ContentTpl(etpl);
@ -112,61 +185,173 @@ namespace Online.Controllers
if (string.IsNullOrEmpty(iframe))
return OnError();
iframe = Regex.Replace(iframe, "token=[a-z0-9]{32}", "token=00000000000000000000000000000000");
iframe = Regex.Replace(iframe, "tv-[0-9]", "tv-2");
var tokenValue = GetEscapedToken();
if (!string.IsNullOrEmpty(tokenValue))
{
if (iframe.Contains("token=", StringComparison.OrdinalIgnoreCase))
iframe = Regex.Replace(iframe, "token=[^&]+", $"token={tokenValue}", RegexOptions.IgnoreCase);
else
iframe = $"{iframe}{(iframe.Contains('?') ? "&" : "?")}token={tokenValue}";
}
iframe = NormalizeIframeParams(iframe);
return await InvkSemaphore($"videoseed:video:{iframe}:{proxyManager?.CurrentProxyIp}", async key =>
{
const string hardUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36";
string iframeHost = Regex.Match(iframe, "(^https?://[^/]+)").Groups[1].Value;
if (string.IsNullOrEmpty(iframeHost))
iframeHost = init.host;
var proxyInfo = proxyManager?.BaseGet() ?? default;
if (!hybridCache.TryGetValue(key, out string location))
{
var headers = httpHeaders(init, HeadersModel.Init
(
("referer", "encrypt:kwwsv=22ylghrvhhg1wy2")
));
var headers = httpHeaders(init);
if (headers != null)
headers.Add(new HeadersModel("User-Agent", hardUserAgent));
else
headers = HeadersModel.Init(("User-Agent", hardUserAgent));
string gotoReferer = string.IsNullOrEmpty(iframeHost) ? init.host : iframeHost;
if (!string.IsNullOrEmpty(gotoReferer) && !gotoReferer.EndsWith("/"))
gotoReferer += "/";
try
{
using (var browser = new PlaywrightBrowser(init.priorityBrowser))
{
var page = await browser.NewPageAsync(init.plugin, proxy: proxy_data, headers: headers?.ToDictionary()).ConfigureAwait(false);
var page = await browser.NewPageAsync(init.plugin, proxy: proxyInfo.data, headers: headers?.ToDictionary()).ConfigureAwait(false);
if (page == null)
return null;
return OnError();
await page.AddInitScriptAsync("localStorage.setItem('pljsquality', '1080p');").ConfigureAwait(false);
await page.RouteAsync("**/*", async route =>
var locationFinal = false;
page.Response += (_, response) =>
{
try
{
if (!string.IsNullOrEmpty(location))
string url = response.Url;
if (!IsMediaUrl(url))
return;
if (response.Status == 200)
{
location = url;
locationFinal = true;
}
else if (response.Status == 301 || response.Status == 302)
{
if (response.Headers.TryGetValue("location", out string redirect))
{
location = ResolveRedirectUrl(url, redirect);
locationFinal = false;
}
}
}
catch { }
};
await page.RouteAsync("**/*", async route =>
{
try
{
var rt = route.Request.ResourceType;
if (rt == "image" || rt == "font" || rt == "stylesheet")
{
await route.AbortAsync();
return;
}
if (route.Request.Url.Contains(".m3u8") || (route.Request.Url.Contains(".mp4") && !route.Request.Url.Contains(".ts")))
location = route.Request.Url;
if (await PlaywrightBase.AbortOrCache(page, route, abortMedia: true, fullCacheJS: true))
return;
await route.ContinueAsync();
}
catch { }
});
var waitMedia = page.WaitForResponseAsync(r => IsMediaUrl(r.Url), new() { Timeout = 15_000 });
var options = new PageGotoOptions()
{
Timeout = 15_000,
WaitUntil = WaitUntilState.NetworkIdle
WaitUntil = WaitUntilState.DOMContentLoaded,
Referer = gotoReferer
};
var result = await page.GotoAsync(iframe, options).ConfigureAwait(false);
PlaywrightBase.WebLog("GOTO", iframe, result != null ? result.Status.ToString() : "null", proxy_data);
try
{
await page.EvaluateAsync(@"() => {
const v = document.querySelector('video');
if (v) {
try { v.muted = true; } catch(e) {}
try { v.play && v.play(); } catch(e) {}
}
const btn = document.querySelector('.vjs-big-play-button, .jw-icon-playback, .plyr__control--overlaid, .playerjs-play, .playerjs__play');
if (btn) { try { btn.click(); } catch(e) {} }
try { document.body && document.body.click(); } catch(e) {}
}");
await page.Mouse.ClickAsync(20, 20);
await page.Keyboard.PressAsync("Space");
}
catch { }
try
{
var mediaResp = await waitMedia.ConfigureAwait(false);
if (mediaResp != null && string.IsNullOrEmpty(location))
{
if (mediaResp.Status == 301 || mediaResp.Status == 302)
{
if (mediaResp.Headers.TryGetValue("location", out var redirect))
location = ResolveRedirectUrl(mediaResp.Url, redirect);
locationFinal = false;
}
else
{
location = mediaResp.Url;
locationFinal = mediaResp.Status == 200;
}
}
}
catch { }
for (int i = 0; i < 40 && !locationFinal; i++)
await Task.Delay(250).ConfigureAwait(false);
if (result != null && string.IsNullOrEmpty(location))
{
string html = await page.ContentAsync().ConfigureAwait(false);
location = Regex.Match(html, "<video preload=\"none\" src=\"(https?://[^\"]+)\"").Groups[1].Value;
if (!location.Contains(".m3u") && !location.Contains(".mp4"))
location = Regex.Match(html, "<source[^>]+src=\"(https?://[^\"]+)\"", RegexOptions.IgnoreCase).Groups[1].Value;
if (string.IsNullOrEmpty(location))
location = Regex.Match(html, "<video[^>]+src=\"(https?://[^\"]+)\"", RegexOptions.IgnoreCase).Groups[1].Value;
if (string.IsNullOrEmpty(location))
location = Regex.Match(html, "(https?:\\\\/\\\\/[^\"'\\s]+\\.m3u8[^\"'\\s]*)", RegexOptions.IgnoreCase).Groups[1].Value;
if (!string.IsNullOrEmpty(location))
{
location = location.Replace("\\/", "/");
if (location.StartsWith("//"))
{
var iframeUri = new Uri(iframe);
location = $"{iframeUri.Scheme}:{location}";
}
else if (location.StartsWith("/"))
{
var iframeUri = new Uri(iframe);
location = new Uri(iframeUri, location).ToString();
}
}
if (string.IsNullOrEmpty(location) || (!location.Contains(".m3u") && !location.Contains(".mp4")))
location = null;
}
@ -179,8 +364,9 @@ namespace Online.Controllers
return OnError();
}
}
catch
catch (Exception ex)
{
PlaywrightBase.WebLog("ERR", iframe, $"{ex.GetType().Name}: {ex.Message}", proxy_data);
return OnError();
}
@ -188,10 +374,22 @@ namespace Online.Controllers
hybridCache.Set(key, location, cacheTime(20));
}
string referer = Regex.Match(iframe, "(^https?://[^/]+)").Groups[1].Value;
var headers_stream = httpHeaders(init.corsHost(), HeadersModel.Join(HeadersModel.Init("referer", referer), init.headers_stream));
string streamReferer = string.IsNullOrEmpty(iframeHost) ? iframe : (iframeHost.EndsWith("/") ? iframeHost : iframeHost + "/");
var headers_stream = HeadersModel.Join(
HeadersModel.Init(
("User-Agent", hardUserAgent),
("Referer", streamReferer),
("Origin", iframeHost),
("Accept", "*/*")
),
init.headers_stream
);
string link = HostStreamProxy(init, location, headers_stream, proxyInfo.proxy, force_streamproxy: true, rch);
if (!string.IsNullOrEmpty(location) && location.Contains(".m3u8", StringComparison.OrdinalIgnoreCase))
link += "#.m3u8";
string link = HostStreamProxy(location, headers: headers_stream);
return ContentTo(VideoTpl.ToJson("play", link, "auto", vast: init.vast));
});
}
@ -203,8 +401,15 @@ namespace Online.Controllers
if (!isOk)
return null;
var root = await httpHydra.Get<JObject>($"{init.corsHost()}/apiv2.php?item={(serial == 1 ? "serial" : "movie")}&token={init.token}" + arg, safety: true);
string apiHost = init.apihost ?? init.host;
string tokenValue = GetEscapedToken() ?? "";
var root = await httpHydra.Get<JObject>(
$"{init.cors(apiHost)}/apiv2.php?item={(serial == 1 ? "serial" : "movie")}&token={tokenValue}" + arg,
safety: true,
addheaders: HeadersModel.Init(
("referer", apiHost),
("origin", apiHost)
));
if (root == null || !root.ContainsKey("data") || root.Value<string>("status") == "error")
{
proxyManager?.Refresh();
@ -214,5 +419,86 @@ namespace Online.Controllers
return root["data"]?.First;
}
#endregion
static string NormalizeIframeParams(string iframe)
{
if (string.IsNullOrEmpty(iframe))
return iframe;
return Regex.Replace(iframe, "default_audio=([^&]+)", match =>
{
string value = match.Groups[1].Value;
if (string.IsNullOrEmpty(value) || value.Contains("%"))
return match.Value;
return $"default_audio={Uri.EscapeDataString(value)}";
}, RegexOptions.IgnoreCase);
}
static string NormalizeToken(string token)
{
if (string.IsNullOrEmpty(token))
return token;
int separatorIndex = token.LastIndexOf('|');
if (separatorIndex >= 0 && separatorIndex < token.Length - 1)
return token[(separatorIndex + 1)..];
return token;
}
static string ExtractDefaultAudio(string iframe)
{
if (string.IsNullOrEmpty(iframe))
return null;
if (Uri.TryCreate(iframe, UriKind.Absolute, out var uri))
{
var query = HttpUtility.ParseQueryString(uri.Query);
return query.Get("default_audio");
}
var match = Regex.Match(iframe, "default_audio=([^&]+)", RegexOptions.IgnoreCase);
return match.Success ? Uri.UnescapeDataString(match.Groups[1].Value) : null;
}
static string ApplyDefaultAudio(string iframe, string defaultAudio)
{
if (string.IsNullOrEmpty(iframe) || string.IsNullOrEmpty(defaultAudio))
return iframe;
string encoded = Uri.EscapeDataString(defaultAudio);
if (iframe.Contains("default_audio=", StringComparison.OrdinalIgnoreCase))
return Regex.Replace(iframe, "default_audio=[^&]+", $"default_audio={encoded}", RegexOptions.IgnoreCase);
return $"{iframe}{(iframe.Contains('?') ? "&" : "?")}default_audio={encoded}";
}
static bool IsMediaUrl(string url)
{
if (string.IsNullOrEmpty(url))
return false;
return url.Contains(".m3u8") || (url.Contains(".mp4") && !url.Contains(".ts"));
}
static string ResolveRedirectUrl(string requestUrl, string redirect)
{
if (string.IsNullOrEmpty(redirect))
return redirect;
if (redirect.StartsWith("//", StringComparison.Ordinal))
{
var requestUri = new Uri(requestUrl);
return $"{requestUri.Scheme}:{redirect}";
}
if (redirect.StartsWith("/", StringComparison.Ordinal))
{
var requestUri = new Uri(requestUrl);
return new Uri(requestUri, redirect).ToString();
}
return redirect;
}
}
}
}

View File

@ -45,7 +45,7 @@ namespace Shared.Engine.Online
string link = null;
var rx = Rx.Split("<li class=\"item\">", content, 1);
var rx = Rx.Matches("<li class=\"[^\"]*\\bitem\\b[^\"]*\">.*?</li>", content, 0, RegexOptions.CultureInvariant | RegexOptions.Singleline);
var similar = new SimilarTpl(rx.Count);
@ -56,7 +56,7 @@ namespace Shared.Engine.Online
string name = row.Match("<div class=\"title\"><[^>]+>([^<]+)");
string _year = row.Match("<span class=\"year\">([0-9]+)");
string img = row.Match("<img src=\"/([^\"]+)\"");
string img = row.Match("<img[^>]+(?:data-src|src)=\"/([^\"]+)\"");
if (!string.IsNullOrEmpty(img))
img = $"{apihost}/{img}";
@ -108,7 +108,7 @@ namespace Shared.Engine.Online
{
try
{
string video = Regex.Match(news, "id=\"playerjsfile\">([^<]+)<").Groups[1].Value;
string video = Regex.Match(news, "id=\"playerjsfile[^\"]*\">([^<]+)<").Groups[1].Value;
if (string.IsNullOrEmpty(video))
{
if (news.Contains("<div class=\"alert\""))

View File

@ -28,14 +28,19 @@ namespace Shared.Models.Templates
Append(item.link, item.quality);
}
}
void EnsureData()
{
data ??= new List<StreamQualityDto>(8);
}
public bool Any() => data.Any();
public bool Any() => data != null && data.Any();
public void Append(string link, string quality)
{
if (string.IsNullOrEmpty(link) || string.IsNullOrEmpty(quality))
return;
EnsureData();
if (InvkEvent.IsStreamQuality())
{
@ -54,6 +59,8 @@ namespace Shared.Models.Templates
{
if (string.IsNullOrEmpty(link) || string.IsNullOrEmpty(quality))
return;
EnsureData();
if (InvkEvent.IsStreamQuality())
{
@ -72,6 +79,8 @@ namespace Shared.Models.Templates
public Dictionary<string, string> ToObject(bool emptyToNull = false)
{
EnsureData();
if (emptyToNull && data.Count == 0)
return null;
@ -85,6 +94,8 @@ namespace Shared.Models.Templates
public string MaxQuality()
{
EnsureData();
if (data.Count == 0)
return string.Empty;
@ -93,6 +104,8 @@ namespace Shared.Models.Templates
public StreamQualityDto Firts()
{
EnsureData();
if (data.Count == 0)
return default;