Skip to main content

kodik_parser/
parser.rs

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    /// Extracts video information from response text.
32    ///
33    /// # Errors
34    ///
35    /// Returns `KodikError::Regex` if any of the required video fields (type, hash, id) are not found in the response text.
36    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    /// Extracts video information from URL.
87    ///
88    /// # Errors
89    ///
90    /// Returns `KodikError::Regex` if the video information (type, hash, id) is not found in the URL.
91    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
123/// Extracts the player URL from response text.
124///
125/// # Errors
126///
127/// Returns `KodikError::Regex` if the player path is not found in the response text.
128///
129/// # Panics
130///
131/// Panics if the regex capture group is not found, which should not happen if the regex is correct.
132pub 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
153/// Extracts the API endpoint from player response text.
154///
155/// # Errors
156///
157/// Returns `KodikError::Regex` if the API endpoint is not found in the player response text.
158///
159/// # Panics
160///
161/// Panics if the regex capture group is not found, which should not happen if the regex is correct.
162pub 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
183/// Parses a Kodik player page asynchronously and returns structured video stream information.
184///
185/// This function performs the complete sequence of operations required to
186/// fetch, extract, and decode player data from a given Kodik URL:
187///
188/// 1. **Domain extraction** – Determines the Kodik domain from the provided URL.
189/// 2. **HTML retrieval** – Downloads the initial page HTML.
190/// 3. **Video info extraction** – Parses the embedded video information payload.
191/// 4. **API endpoint resolution** – If not cached, discovers the video info API endpoint.
192/// 5. **Player data request** – Sends a POST request to retrieve player data.
193/// 6. **Link decoding** – Decrypts and normalizes streaming URLs.
194///
195/// The function uses a cached `VIDEO_INFO_ENDPOINT` to avoid repeated endpoint lookups.
196///
197/// # Arguments
198/// * `client` – An [`reqwest::Client`] used for making HTTP requests.
199/// * `url` – A full Kodik player page URL.
200///
201/// # Returns
202/// A [`KodikResponse`] containing structured player metadata and stream URLs.
203///
204/// # Errors
205/// Returns an error if:
206/// - The domain cannot be extracted from the URL.
207/// - Network requests fail.
208/// - HTML parsing fails due to unexpected format changes.
209/// - The API endpoint cannot be found.
210/// - Link decoding fails.
211///
212/// # Example
213/// ```no_run
214/// use kodik_parser::reqwest::Client;
215///
216/// # async fn run() {
217/// let client = Client::new();
218/// let url = "https://kodikplayer.com/some-type/some-id/some-hash/some-quality";
219/// let kodik_response = kodik_parser::parse(&client, url).await.unwrap();
220///
221/// let link_720 = &kodik_response.links.quality_720.first().unwrap().src;
222/// println!("Link with 720p quality is: {link_720}");
223/// # }
224/// ```
225pub 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}