using Microsoft.AspNetCore.Mvc; using Microsoft.Playwright; using System.Linq; using Newtonsoft.Json.Linq; using Shared.PlaywrightCore; namespace Online.Controllers { public class Videoseed : BaseOnlineController { 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 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(); if (await IsRequestBlocked(rch: false)) return badInitMsg; if (string.IsNullOrEmpty(init.token)) return OnError(); return await InvkSemaphore($"videoseed:view:{kinopoisk_id}:{imdb_id}:{original_title}", async key => { #region search if (!hybridCache.TryGetValue(key, out (Dictionary seasons, string iframe, Dictionary translations) cache)) { 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>() ?.ToDictionary(item => item.Key, item => item.Value?.Value("iframe")); if (serial == 1) cache.seasons = data?["seasons"]?.ToObject>(); else { cache.iframe = data?.Value("iframe"); } if (cache.seasons == null && string.IsNullOrEmpty(cache.iframe)) { proxyManager?.Refresh(); return OnError(); } proxyManager?.Success(); hybridCache.Set(key, cache, cacheTime(40)); } #endregion if (cache.iframe != null) { #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 } else { #region Сериал string enc_title = HttpUtility.UrlEncode(title); string enc_original_title = HttpUtility.UrlEncode(original_title); if (s == -1) { var tpl = new SeasonTpl(cache.seasons.Count); 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); } return await ContentTpl(tpl); } 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>(); var etpl = hasTranslations ? new EpisodeTpl(vtpl, videos.Count) : new EpisodeTpl(videos.Count); 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("iframe"); 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); } #endregion } }); } #region Video [HttpGet] [Route("lite/videoseed/video/{*iframe}")] async public ValueTask Video(string iframe) { if (await IsRequestBlocked(rch: false)) return badInitMsg; iframe = AesTo.Decrypt(iframe); if (string.IsNullOrEmpty(iframe)) return OnError(); 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); 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: proxyInfo.data, headers: headers?.ToDictionary()).ConfigureAwait(false); if (page == null) return OnError(); await page.AddInitScriptAsync("localStorage.setItem('pljsquality', '1080p');").ConfigureAwait(false); var locationFinal = false; page.Response += (_, response) => { try { 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; } 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.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, "]+src=\"(https?://[^\"]+)\"", RegexOptions.IgnoreCase).Groups[1].Value; if (string.IsNullOrEmpty(location)) location = Regex.Match(html, "]+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; } PlaywrightBase.WebLog("SET", iframe, location, proxy_data); } if (string.IsNullOrEmpty(location)) { proxyManager?.Refresh(); return OnError(); } } catch (Exception ex) { PlaywrightBase.WebLog("ERR", iframe, $"{ex.GetType().Name}: {ex.Message}", proxy_data); return OnError(); } proxyManager?.Success(); hybridCache.Set(key, location, cacheTime(20)); } 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"; return ContentTo(VideoTpl.ToJson("play", link, "auto", vast: init.vast)); }); } #endregion #region goSearch async Task goSearch(int serial, bool isOk, string arg) { if (!isOk) return null; string apiHost = init.apihost ?? init.host; string tokenValue = GetEscapedToken() ?? ""; var root = await httpHydra.Get( $"{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("status") == "error") { proxyManager?.Refresh(); return null; } 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; } } }