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