using HtmlAgilityPack; using Shared.Engine.RxEnumerate; using Shared.Models.Base; using Shared.Models.Online.Kodik; using Shared.Models.Online.Settings; using Shared.Models.Templates; using System.Collections.Concurrent; using System.Text; using System.Text.RegularExpressions; using System.Web; namespace Shared.Engine.Online { public struct KodikInvoke { #region KodikInvoke static readonly ConcurrentDictionary psingles = new (); static readonly IHybridCache hybridCache = IHybridCache.Get(null); readonly IEnumerable fallbackDatabase; string host; string apihost, token, videopath; bool usehls, cdn_is_working; HttpHydra httpHydra; Func onstreamfile; Action requesterror; public KodikInvoke(string host, KodikSettings init, string videopath, IEnumerable fallbackDatabase, HttpHydra httpHydra, Func onstreamfile, Action requesterror = null) { this.host = host != null ? $"{host}/" : null; this.apihost = init.apihost; this.token = init.token; this.videopath = videopath; this.fallbackDatabase = fallbackDatabase; this.httpHydra = httpHydra; this.onstreamfile = onstreamfile; this.usehls = init.hls; this.cdn_is_working = init.cdn_is_working; this.requesterror = requesterror; } #endregion #region Embed async public Task> Embed(string imdb_id, long kinopoisk_id, int s) { if (string.IsNullOrEmpty(imdb_id) && kinopoisk_id == 0) return null; List results = null; if (!string.IsNullOrWhiteSpace(token)) { string url = $"{apihost}/search?token={token}&limit=100&with_episodes=true"; if (kinopoisk_id > 0) url += $"&kinopoisk_id={kinopoisk_id}"; if (!string.IsNullOrWhiteSpace(imdb_id)) url += $"&imdb_id={imdb_id}"; if (s > 0) url += $"&season={s}"; var root = await httpHydra.Get(url, safety: true); if (root?.results != null) results = root.results; else requesterror?.Invoke(); } if (results == null) results = FallbackByIds(imdb_id, kinopoisk_id, s); return results; } public async Task Embed(string title, string original_title, int clarification) { try { if (string.IsNullOrEmpty(title) && string.IsNullOrEmpty(original_title)) return null; List results = null; if (!string.IsNullOrWhiteSpace(token)) { string url = $"{apihost}/search?token={token}&limit=100&title={HttpUtility.UrlEncode(original_title ?? title)}&with_episodes=true&with_material_data=true"; var root = await httpHydra.Get(url, safety: true); if (root?.results != null) results = root.results; else requesterror?.Invoke(); } if (results == null) results = FallbackByTitle(title, original_title); if (results == null) return null; var hash = new HashSet(20); var stpl = new SimilarTpl(results.Count); string enc_title = HttpUtility.UrlEncode(title); string enc_original_title = HttpUtility.UrlEncode(original_title); foreach (var similar in results) { string pick = similar.title?.ToLowerAndTrim(); if (string.IsNullOrEmpty(pick)) continue; if (hash.Contains(pick)) continue; hash.Add(pick); string name = !string.IsNullOrEmpty(similar.title) && !string.IsNullOrEmpty(similar.title_orig) ? $"{similar.title} / {similar.title_orig}" : (similar.title ?? similar.title_orig); string details = similar.translation.title; if (similar.last_season > 0) details += $"{SimilarTpl.OnlineSplit} {similar.last_season}й сезон"; var matd = similar.material_data; string img = PosterApi.Size(matd.anime_poster_url ?? matd.drama_poster_url ?? matd.poster_url); stpl.Append(name, similar.year?.ToString(), details, host + $"lite/kodik?title={enc_title}&original_title={enc_original_title}&clarification={clarification}&pick={HttpUtility.UrlEncode(pick)}", img); } return new EmbedModel() { stpl = stpl, result = results }; } catch { return null; } } public List Embed(List results, string pick) { var content = new List(results.Count); foreach (var i in results) { if (i.title == null || i.title.ToLowerAndTrim() != pick) continue; content.Add(i); } return content; } #endregion #region VideoParse async public Task> VideoParse(string linkhost, string link) { List streams = null; string player_single = null; string domain = null, d_sign = null, pd = null, pd_sign = null, ref_domain = null, ref_sign = null, type = null, hash = null, id = null; await httpHydra.GetSpan($"https:{link}", iframe => { player_single = Rx.Match(iframe, "src=\"/(assets/js/app\\.player_[^\"]+\\.js)\""); var advertDebug = Rx.Split("advertDebug", iframe); if (advertDebug.Count > 1) { var preview = Rx.Split("preview-icons", advertDebug[1].Span); if (preview.Count > 0) { string _frame = Regex.Replace(preview[0].ToString(), "[\n\r\t ]+", ""); domain = Regex.Match(_frame, "domain=\"([^\"]+)\"").Groups[1].Value; d_sign = Regex.Match(_frame, "d_sign=\"([^\"]+)\"").Groups[1].Value; pd = Regex.Match(_frame, "pd=\"([^\"]+)\"").Groups[1].Value; pd_sign = Regex.Match(_frame, "pd_sign=\"([^\"]+)\"").Groups[1].Value; ref_domain = Regex.Match(_frame, "ref=\"([^\"]+)\"").Groups[1].Value; ref_sign = Regex.Match(_frame, "ref_sign=\"([^\"]+)\"").Groups[1].Value; type = Regex.Match(_frame, "videoInfo.type='([^']+)'").Groups[1].Value; hash = Regex.Match(_frame, "videoInfo.hash='([^']+)'").Groups[1].Value; id = Regex.Match(_frame, "videoInfo.id='([^']+)'").Groups[1].Value; } } }); if (string.IsNullOrEmpty(domain) || string.IsNullOrEmpty(player_single)) { requesterror?.Invoke(); return null; } string uri = null; if (!psingles.TryGetValue(player_single, out uri)) { await httpHydra.GetSpan($"{linkhost}/{player_single}", playerjs => { uri = DecodeUrlBase64(Rx.Match(playerjs, "type:\"POST\",url:atob\\(\"([^\"]+)\"\\)")); if (!string.IsNullOrEmpty(uri)) psingles.TryAdd(player_single, uri); }); } if (string.IsNullOrEmpty(uri)) { requesterror?.Invoke(); return null; } bool _usehls = usehls; await httpHydra.PostSpan($"{linkhost + uri}", $"d={domain}&d_sign={d_sign}&pd={pd}&pd_sign={pd_sign}&ref={ref_domain}&ref_sign={ref_sign}&bad_user=false&cdn_is_working={cdn_is_working.ToString().ToLower()}&type={type}&hash={hash}&id={id}&info=%7B%7D", json => { var rx = Rx.Matches("\"([0-9]+)p?\":\\[\\{\"src\":\"([^\"]+)", json); if (rx.Count == 0) return; streams = new List(rx.Count); foreach (var row in rx.Rows()) { var g = row.Groups(); if (!string.IsNullOrWhiteSpace(g[2].Value)) { string m3u = g[2].Value; if (!m3u.Contains("manifest.m3u8")) { int zCharCode = Convert.ToInt32('Z'); string src = Regex.Replace(g[2].Value, "[a-zA-Z]", e => { int eCharCode = Convert.ToInt32(e.Value[0]); return ((eCharCode <= zCharCode ? 90 : 122) >= (eCharCode = eCharCode + 18) ? (char)eCharCode : (char)(eCharCode - 26)).ToString(); }); m3u = DecodeUrlBase64(src); } if (m3u.StartsWith("//")) m3u = $"https:{m3u}"; if (!_usehls && m3u.Contains(".m3u")) m3u = m3u.Replace(":hls:manifest.m3u8", ""); streams.Add(new StreamModel() { q = $"{g[1].Value}p", url = m3u }); } } }); if (streams == null || streams.Count == 0) { requesterror?.Invoke(); return null; } streams.Reverse(); return streams; } public string VideoParse(List streams, string title, string original_title, int episode, bool play, VastConf vast = null) { if (streams == null || streams.Count == 0) return string.Empty; if (play) return onstreamfile(streams[0].url); string name = title ?? original_title ?? "auto"; if (episode > 0) name += $" ({episode} серия)"; var streamquality = new StreamQualityTpl(); foreach (var l in streams) streamquality.Append(onstreamfile(l.url), l.q); return VideoTpl.ToJson("play", onstreamfile(streams[0].url), name, streamquality: streamquality, vast: vast); } #endregion #region Tpl public async Task Tpl(List results, string args, string imdb_id, long kinopoisk_id, string title, string original_title, int clarification, string pick, string kid, int s, bool showstream, bool rjson) { string enc_title = HttpUtility.UrlEncode(title); string enc_original_title = HttpUtility.UrlEncode(original_title); if (results[0].type is "foreign-movie" or "soviet-cartoon" or "foreign-cartoon" or "russian-cartoon" or "anime" or "russian-movie") { #region Фильм var mtpl = new MovieTpl(title, original_title, results.Count); foreach (var data in results) { string url = host + $"lite/kodik/video?title={enc_title}&original_title={enc_original_title}&link={HttpUtility.UrlEncode(data.link)}"; string streamlink = null; if (showstream) { streamlink = usehls ? $"{url.Replace("/video", $"/{videopath}.m3u8")}&play=true" : $"{url.Replace("/video", $"/{videopath}")}&play=true"; if (!string.IsNullOrEmpty(args)) streamlink += $"&{args.Remove(0, 1)}"; } mtpl.Append(data.translation.title, url, "call", streamlink); } return mtpl; #endregion } else { #region Сериал string enc_pick = HttpUtility.UrlEncode(pick); if (s == -1) { var tpl = new SeasonTpl(results.Count); var hash = new HashSet(); foreach (var item in results.AsEnumerable().Reverse()) { int season = item.last_season; string link = host + $"lite/kodik?rjson={rjson}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={enc_title}&original_title={enc_original_title}&clarification={clarification}&pick={enc_pick}&s={season}"; if (hash.Contains(season)) continue; hash.Add(season); tpl.Append($"{season} сезон", link, season); } return tpl; } else { #region Перевод var vtpl = new VoiceTpl(); HashSet hash = new HashSet(); foreach (var item in results) { string id = item.id; if (string.IsNullOrEmpty(id)) continue; string name = item.translation.title ?? "оригинал"; if (hash.Contains(name)) continue; if (item.last_season != s) { if (item.seasons == null || !item.seasons.ContainsKey(s.ToString())) continue; } hash.Add(name); if (string.IsNullOrEmpty(kid)) kid = id; string link = host + $"lite/kodik?rjson={rjson}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={enc_title}&original_title={enc_original_title}&clarification={clarification}&pick={enc_pick}&s={s}&kid={id}"; vtpl.Append(name, kid == id, link); } #endregion var selected = results.FirstOrDefault(i => i.id == kid); if (string.IsNullOrEmpty(selected.id)) selected = results[0]; var series = await ResolveEpisodesAsync(selected, s); if (series == null || series.Count == 0) return default; var etpl = new EpisodeTpl(vtpl, series.Count); string sArhc = s.ToString(); foreach (var episode in series) { string url = host + $"lite/kodik/video?title={enc_title}&original_title={enc_original_title}&link={HttpUtility.UrlEncode(episode.Value)}&episode={episode.Key}"; string streamlink = null; if (showstream) { streamlink = usehls ? $"{url.Replace("/video", $"/{videopath}.m3u8")}&play=true" : $"{url.Replace("/video", $"/{videopath}")}&play=true"; if (!string.IsNullOrEmpty(args)) streamlink += $"&{args.Remove(0, 1)}"; } etpl.Append($"{episode.Key} серия", title ?? original_title, sArhc, episode.Key, url, "call", streamlink: streamlink); } return etpl; } #endregion } } #endregion #region DecodeUrlBase64 static string DecodeUrlBase64(string s) { if (s == null) return s; return Encoding.UTF8.GetString(Convert.FromBase64String(s.Replace('-', '+').Replace('_', '/').PadRight(4 * ((s.Length + 3) / 4), '='))); } #endregion #region [Codex AI] - db.json List FallbackByIds(string imdb_id, long kinopoisk_id, int season) { var data = fallbackDatabase; if (data == null) return null; bool requireImdb = !string.IsNullOrEmpty(imdb_id); bool requireKinopoisk = kinopoisk_id > 0; var matches = data.Where(item => { bool imdbMatch = !requireImdb || string.Equals(item.imdb_id, imdb_id, StringComparison.OrdinalIgnoreCase); bool kinopoiskMatch = !requireKinopoisk || item.kinopoisk_id == kinopoisk_id.ToString(); return imdbMatch && kinopoiskMatch; }).ToList(); if (matches.Count == 0) return null; return matches.Count == 0 ? null : matches; } List FallbackByTitle(string title, string originalTitle) { var data = fallbackDatabase; if (data == null) return null; bool hasTitle = !string.IsNullOrWhiteSpace(title); bool hasOriginal = !string.IsNullOrWhiteSpace(originalTitle); var strictMatches = new List(); List fallbackMatches = (hasTitle || hasOriginal) ? new List() : null; foreach (var item in data) { bool titleMatch = !hasTitle || TitleMatches(item.title, title) || TitleMatches(item.title_orig, title); bool originalMatch = !hasOriginal || TitleMatches(item.title, originalTitle) || TitleMatches(item.title_orig, originalTitle); if (titleMatch && originalMatch) { strictMatches.Add(item); continue; } if (fallbackMatches == null) continue; if (TitleMatches(item.title, title) || TitleMatches(item.title_orig, title) || TitleMatches(item.title, originalTitle) || TitleMatches(item.title_orig, originalTitle)) { fallbackMatches.Add(item); } } var matches = strictMatches.Count > 0 ? strictMatches : fallbackMatches; return matches == null || matches.Count == 0 ? null : matches; } static bool TitleMatches(string source, string target) { if (string.IsNullOrWhiteSpace(source) || string.IsNullOrWhiteSpace(target)) return false; string normalizedSource = StringConvert.SearchName(source); string normalizedTarget = StringConvert.SearchName(target); if (string.IsNullOrWhiteSpace(normalizedSource) || string.IsNullOrWhiteSpace(normalizedTarget)) return false; return normalizedSource.Contains(normalizedTarget); } async Task> ResolveEpisodesAsync(Result selected, int season) { if (season <= 0) return null; string seasonKey = season.ToString(); if (selected.seasons != null && selected.seasons.TryGetValue(seasonKey, out var seasonInfo) && seasonInfo.episodes != null && seasonInfo.episodes.Count > 0) { return seasonInfo.episodes; } var seasonsFromHtml = await LoadSeasonsFromHtml(selected); if (seasonsFromHtml != null && seasonsFromHtml.TryGetValue(seasonKey, out seasonInfo) && seasonInfo.episodes != null && seasonInfo.episodes.Count > 0) { return seasonInfo.episodes; } return null; } async Task> LoadSeasonsFromHtml(Result selected) { if (string.IsNullOrWhiteSpace(selected.id) || string.IsNullOrWhiteSpace(selected.link) || httpHydra == null) return null; string cacheKey = $"kodik:series:{selected.id}"; if (hybridCache.TryGetValue(cacheKey, out Dictionary cached)) return cached; try { string html = await httpHydra.Get($"https:{selected.link}"); if (string.IsNullOrWhiteSpace(html)) { requesterror?.Invoke(); return null; } var doc = new HtmlDocument(); doc.LoadHtml(html); var optionsRoot = doc.DocumentNode.SelectSingleNode("//div[contains(@class,'series-options')]"); if (optionsRoot == null) return null; var seasons = new Dictionary(); var seasonNodes = optionsRoot.SelectNodes(".//div[contains(@class,'season-')]"); if (seasonNodes == null) return null; foreach (var seasonNode in seasonNodes) { string classes = seasonNode.GetAttributeValue("class", string.Empty); var match = Regex.Match(classes, "season-([0-9]+)"); if (!match.Success) continue; string seasonKey = match.Groups[1].Value; if (string.IsNullOrEmpty(seasonKey)) continue; var options = seasonNode.SelectNodes(".//option"); if (options == null || options.Count == 0) continue; var episodes = new Dictionary(); foreach (var option in options) { string episodeNumber = option.GetAttributeValue("value", null) ?? option.InnerText; episodeNumber = episodeNumber?.Trim(); if (string.IsNullOrEmpty(episodeNumber)) continue; string episodeLink = BuildEpisodeLink(option); if (string.IsNullOrEmpty(episodeLink)) continue; if (!episodes.ContainsKey(episodeNumber)) episodes[episodeNumber] = episodeLink; } if (episodes.Count > 0) { seasons[seasonKey] = new Season { link = selected.link, episodes = episodes }; } } if (seasons.Count == 0) return null; hybridCache.Set(cacheKey, seasons, TimeSpan.FromMinutes(20)); return seasons; } catch { return null; } } static string BuildEpisodeLink(HtmlNode option) { string dataId = option.GetAttributeValue("data-id", null); string dataHash = option.GetAttributeValue("data-hash", null); if (string.IsNullOrWhiteSpace(dataId) || string.IsNullOrWhiteSpace(dataHash)) return null; return $"//kodik.info/seria/{dataId}/{dataHash}/720p"; } #endregion } }