1use crate::decoder;
2use crate::scraper;
3use crate::{KODIK_STATE, Response};
4use kodik_utils::KodikError;
5use lazy_regex::Lazy;
6use regex_lite::Regex;
7use reqwest::Client;
8use serde::Serialize;
9
10#[derive(Debug, Serialize, PartialEq, Eq)]
11pub struct VideoInfo<'a> {
12 r#type: &'a str,
13 hash: &'a str,
14 id: &'a str,
15 bad_user: &'static str,
16 info: &'static str,
17 cdn_is_working: &'static str,
18}
19
20impl<'a> VideoInfo<'a> {
21 #[must_use]
22 pub(crate) const fn new(r#type: &'a str, hash: &'a str, id: &'a str) -> Self {
23 Self {
24 r#type,
25 hash,
26 id,
27 bad_user: "True",
28 info: "{}",
29 cdn_is_working: "True",
30 }
31 }
32
33 pub(crate) fn from_response(html: &'_ str) -> Result<VideoInfo<'_>, KodikError> {
39 let from_response_re: &Lazy<Regex> =
40 lazy_regex::regex!(r"\.(?P<field>type|hash|id) = '(?P<value>.*?)';");
41
42 log::debug!("Extracting video info from response...");
43
44 let mut r#type = None;
45 let mut hash = None;
46 let mut id = None;
47
48 for caps in from_response_re.captures_iter(html) {
49 match &caps["field"] {
50 "type" => {
51 r#type = Some(
52 caps.name("value")
53 .ok_or(KodikError::Regex("videoInfo.type value not found"))?
54 .as_str(),
55 );
56 }
57 "hash" => {
58 hash = Some(
59 caps.name("value")
60 .ok_or(KodikError::Regex("videoInfo.hash value not found"))?
61 .as_str(),
62 );
63 }
64 "id" => {
65 id = Some(
66 caps.name("value")
67 .ok_or(KodikError::Regex("videoInfo.id value not found"))?
68 .as_str(),
69 );
70 }
71 _ => {}
72 }
73 }
74
75 let video_info = VideoInfo::new(
76 r#type.ok_or(KodikError::Regex("videoInfo.type not found"))?,
77 hash.ok_or(KodikError::Regex("videoInfo.hash not found"))?,
78 id.ok_or(KodikError::Regex("videoInfo.id not found"))?,
79 );
80 log::trace!("Extracted video info: {video_info:#?}");
81
82 Ok(video_info)
83 }
84
85 pub(crate) fn from_url(url: &'_ str) -> Result<VideoInfo<'_>, KodikError> {
91 let from_url_re: &Lazy<Regex> = lazy_regex::regex!(r"/([^/]+)/(\d+)/([a-z0-9]+)");
92
93 log::debug!("Extracting video info from url...");
94
95 let caps = from_url_re
96 .captures(url)
97 .ok_or(KodikError::Regex("videoInfo not found"))?;
98
99 let r#type = caps
100 .get(1)
101 .ok_or(KodikError::Regex("videoInfo.type not found"))?
102 .as_str();
103 let id = caps
104 .get(2)
105 .ok_or(KodikError::Regex("videoInfo.id not found"))?
106 .as_str();
107 let hash = caps
108 .get(3)
109 .ok_or(KodikError::Regex("videoInfo.hash not found"))?
110 .as_str();
111
112 Ok(VideoInfo::new(r#type, hash, id))
113 }
114}
115
116pub fn extract_player_url(domain: &str, html: &str) -> Result<String, KodikError> {
126 let player_path_re: &Lazy<Regex> = lazy_regex::regex!(
127 r#"<script\s*type="text/javascript"\s*src="/(assets/js/app\.player_single[^"]*)""#
128 );
129
130 log::debug!("Extracting player url...");
131 let player_path = player_path_re
132 .captures(html)
133 .ok_or(KodikError::Regex(
134 "there is no player path in response text",
135 ))?
136 .get(1)
137 .ok_or(KodikError::Regex("player path capture group not found"))?
138 .as_str();
139 log::trace!("Extracted player url: {player_path}");
140
141 Ok(format!("https://{domain}/{player_path}"))
142}
143
144pub fn extract_endpoint(html: &str) -> Result<String, KodikError> {
154 let endpoint_re: &Lazy<Regex> =
155 lazy_regex::regex!(r#"\$\.ajax\([^>]+,url:\s*atob\(["\']([\w=]+)["\']\)"#);
156
157 log::debug!("Extracting endpoint...");
158 let encoded_endpoint = endpoint_re
159 .captures(html)
160 .ok_or(KodikError::Regex(
161 "there is no api endpoint in player response",
162 ))?
163 .get(1)
164 .ok_or(KodikError::Regex("api endpoint capture group not found"))?
165 .as_str();
166
167 let endpoint = decoder::decode_base64(encoded_endpoint)?;
168 log::trace!("Extracted endpoint: {endpoint}");
169
170 Ok(endpoint)
171}
172
173pub async fn parse(client: &Client, url: &str) -> Result<Response, KodikError> {
216 let domain = kodik_utils::extract_domain(url)?;
217 let mut html = String::new();
218
219 let video_info = if let Ok(video_info) = VideoInfo::from_url(url) {
220 video_info
221 } else {
222 html = scraper::get(client, url).await?;
223 VideoInfo::from_response(&html)?
224 };
225
226 loop {
227 let endpoint = KODIK_STATE.endpoint();
228
229 if !endpoint.is_empty() {
230 if let Ok(mut kodik_response) =
231 scraper::post(client, domain, &endpoint, &video_info).await
232 {
233 decoder::decode_links(&mut kodik_response)?;
234 return Ok(kodik_response);
235 }
236 KODIK_STATE.clear_endpoint();
237 continue;
238 }
239
240 if KODIK_STATE.try_begin_update() {
241 log::warn!("Endpoint not found in cache, updating...");
242 let fetched;
243 let page_html = if html.is_empty() {
244 fetched = scraper::get(client, url).await?;
245 &fetched
246 } else {
247 &html
248 };
249 let player_url = extract_player_url(domain, page_html)?;
250 let player_html = scraper::get(client, &player_url).await?;
251 let new_endpoint = extract_endpoint(&player_html)?;
252 KODIK_STATE.finish_update(new_endpoint);
253 continue;
254 }
255
256 KODIK_STATE.wait_for_update().await;
257 }
258}