Skip to main content

kodik_parser/
parser.rs

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    /// Extracts video information from response text.
34    ///
35    /// # Errors
36    ///
37    /// Returns `KodikError::Regex` if any of the required video fields (type, hash, id) are not found in the response text.
38    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    /// Extracts video information from URL.
86    ///
87    /// # Errors
88    ///
89    /// Returns `KodikError::Regex` if the video information (type, hash, id) is not found in the URL.
90    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
116/// Extracts the player URL from response text.
117///
118/// # Errors
119///
120/// Returns `KodikError::Regex` if the player path is not found in the response text.
121///
122/// # Panics
123///
124/// Panics if the regex capture group is not found, which should not happen if the regex is correct.
125pub 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
144/// Extracts the API endpoint from player response text.
145///
146/// # Errors
147///
148/// Returns `KodikError::Regex` if the API endpoint is not found in the player response text.
149///
150/// # Panics
151///
152/// Panics if the regex capture group is not found, which should not happen if the regex is correct.
153pub 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
173/// Parses a Kodik player page asynchronously and returns structured video stream information.
174///
175/// This function performs the complete sequence of operations required to
176/// fetch, extract, and decode player data from a given Kodik URL:
177///
178/// 1. **Domain extraction** – Determines the Kodik domain from the provided URL.
179/// 2. **HTML retrieval** – Downloads the initial page HTML.
180/// 3. **Video info extraction** – Parses the embedded video information payload.
181/// 4. **API endpoint resolution** – If not cached, discovers the video info API endpoint.
182/// 5. **Player data request** – Sends a POST request to retrieve player data.
183/// 6. **Link decoding** – Decrypts and normalizes streaming URLs.
184///
185/// The function uses a cached `VIDEO_INFO_ENDPOINT` to avoid repeated endpoint lookups.
186///
187/// # Arguments
188/// * `client` – An [`reqwest::Client`] used for making HTTP requests.
189/// * `url` – A full Kodik player page URL.
190///
191/// # Returns
192/// A [`KodikResponse`] containing structured player metadata and stream URLs.
193///
194/// # Errors
195/// Returns an error if:
196/// - The domain cannot be extracted from the URL.
197/// - Network requests fail.
198/// - HTML parsing fails due to unexpected format changes.
199/// - The API endpoint cannot be found.
200/// - Link decoding fails.
201///
202/// # Example
203/// ```no_run
204/// use kodik_parser::reqwest::Client;
205///
206/// # async fn run() {
207/// let client = Client::new();
208/// let url = "https://kodikplayer.com/some-type/some-id/some-hash/some-quality";
209/// let kodik_response = kodik_parser::parse(&client, url).await.unwrap();
210///
211/// let link_720 = &kodik_response.links.quality_720.first().unwrap().src;
212/// println!("Link with 720p quality is: {link_720}");
213/// # }
214/// ```
215pub 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}