Skip to main content

kodik_parser/
parser.rs

1use std::{
2    array::IntoIter,
3    sync::{LazyLock, RwLock},
4};
5
6use crate::decoder;
7use crate::error::KodikError;
8use regex::Regex;
9use serde::Serialize;
10pub static VIDEO_INFO_ENDPOINT: RwLock<String> = RwLock::new(String::new());
11
12#[derive(Debug, Serialize, PartialEq, Eq)]
13pub struct VideoInfo<'a> {
14    r#type: &'a str,
15    hash: &'a str,
16    id: &'a str,
17    bad_user: &'static str,
18    info: &'static str,
19    cdn_is_working: &'static str,
20}
21
22impl<'a> VideoInfo<'a> {
23    pub const fn new(r#type: &'a str, hash: &'a str, id: &'a str) -> Self {
24        Self {
25            r#type,
26            hash,
27            id,
28            bad_user: "True",
29            info: "{}",
30            cdn_is_working: "True",
31        }
32    }
33
34    fn iter(&'a self) -> IntoIter<(&'a str, &'a str), 6> {
35        [
36            ("type", self.r#type),
37            ("hash", self.hash),
38            ("id", self.id),
39            ("bad_user", self.bad_user),
40            ("info", self.info),
41            ("cdn_is_working", self.cdn_is_working),
42        ]
43        .into_iter()
44    }
45}
46
47impl<'a> IntoIterator for &'a VideoInfo<'a> {
48    type Item = (&'a str, &'a str);
49    type IntoIter = IntoIter<Self::Item, 6>;
50
51    fn into_iter(self) -> Self::IntoIter {
52        self.iter()
53    }
54}
55
56pub fn get_domain(url: &str) -> Result<&str, KodikError> {
57    static DOMAIN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
58        Regex::new(r"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]").unwrap()
59    });
60
61    let domain = DOMAIN_REGEX
62        .find(url)
63        .ok_or(KodikError::Regex("no valid domain found"))?;
64
65    Ok(domain.as_str())
66}
67
68pub fn extract_video_info(response_text: &'_ str) -> Result<VideoInfo<'_>, KodikError> {
69    static VIDEO_INFO_REGEX: LazyLock<Regex> =
70        LazyLock::new(|| Regex::new(r"\.(?P<field>type|hash|id) = '(?P<value>.*?)';").unwrap());
71
72    let (r#type, hash, id) = {
73        let mut video_type = None;
74        let mut hash = None;
75        let mut id = None;
76
77        for caps in VIDEO_INFO_REGEX.captures_iter(response_text) {
78            match &caps["field"] {
79                "type" => {
80                    video_type = Some(
81                        caps.name("value")
82                            .ok_or(KodikError::Regex("videoInfo.type value not found"))?
83                            .as_str(),
84                    );
85                }
86                "hash" => {
87                    hash = Some(
88                        caps.name("value")
89                            .ok_or(KodikError::Regex("videoInfo.hash value not found"))?
90                            .as_str(),
91                    );
92                }
93                "id" => {
94                    id = Some(
95                        caps.name("value")
96                            .ok_or(KodikError::Regex("videoInfo.id value not found"))?
97                            .as_str(),
98                    );
99                }
100                _ => {}
101            }
102        }
103
104        (
105            video_type.ok_or(KodikError::Regex("videoInfo.type not found"))?,
106            hash.ok_or(KodikError::Regex("videoInfo.hash not found"))?,
107            id.ok_or(KodikError::Regex("videoInfo.id not found"))?,
108        )
109    };
110
111    Ok(VideoInfo::new(r#type, hash, id))
112}
113
114pub fn extract_player_url(domain: &str, response_text: &str) -> Result<String, KodikError> {
115    static PLAYER_PATH_REGEX: LazyLock<Regex> = LazyLock::new(|| {
116        Regex::new(r#"<script\s*type="text/javascript"\s*src="/(assets/js/app\.player_single[^"]*)""#).unwrap()
117    });
118
119    let player_path = PLAYER_PATH_REGEX
120        .captures(response_text)
121        .ok_or(KodikError::Regex("there is no player path in response text"))?
122        .get(1)
123        .unwrap()
124        .as_str();
125
126    Ok(format!("https://{domain}/{player_path}"))
127}
128
129pub fn get_api_endpoint(kodik_response_text: &str) -> Result<String, KodikError> {
130    static ENDPOINT_REGEX: LazyLock<Regex> =
131        LazyLock::new(|| Regex::new(r#"\$\.ajax\([^>]+,url:\s*atob\(["\']([\w=]+)["\']\)"#).unwrap());
132
133    let encoded_api_endpoint = ENDPOINT_REGEX
134        .captures(kodik_response_text)
135        .ok_or(KodikError::Regex("there is no api endpoint in player response"))?
136        .get(1)
137        .unwrap()
138        .as_str();
139
140    decoder::b64(encoded_api_endpoint)
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn getting_domain() {
149        let url_with_scheme = "https://kodik.info/video/91873/060cab655974d46835b3f4405807acc2/720p";
150        let url_without_scheme = "kodik.info/video/91873/060cab655974d46835b3f4405807acc2/720p";
151
152        assert_eq!("kodik.info", get_domain(url_with_scheme).unwrap());
153        assert_eq!("kodik.info", get_domain(url_without_scheme).unwrap());
154    }
155
156    #[test]
157    fn extracting_video_info() {
158        let expected_video_info = VideoInfo::new("video", "060cab655974d46835b3f4405807acc2", "91873");
159
160        let response_text = "
161  var videoInfo = {};
162   vInfo.type = 'video';
163   vInfo.hash = '060cab655974d46835b3f4405807acc2';
164   vInfo.id = '91873';
165</script>";
166
167        let video_info = extract_video_info(response_text).unwrap();
168
169        assert_eq!(expected_video_info, video_info);
170    }
171
172    #[test]
173    fn getting_player_url() {
174        let domain = "kodik.info";
175        let response_text = r#"
176  </script>
177
178  <link rel="stylesheet" href="/assets/css/app.player.ffc43caed0b4bc0a9f41f95c06cd8230d49aaf7188dbba5f0770513420541101.css">
179  <script type="text/javascript" src="/assets/js/app.player_single.0a909e421830a88800354716d562e21654500844d220805110c7cf2092d70b05.js"></script>
180</head>
181<body class=" ">
182  <div class="main-box">
183    <style>
184  .resume-button { color: rgba(255, 255, 255, 0.75); }
185  .resume-button:hover { background-color: #171717; }
186  .resume-button { border-radius: 3px; }
187  .active-player .resume-button { border-radius: 3px; }"#;
188
189        let player_url = extract_player_url(domain, response_text).unwrap();
190        assert_eq!(
191            "https://kodik.info/assets/js/app.player_single.0a909e421830a88800354716d562e21654500844d220805110c7cf2092d70b05.js",
192            player_url
193        );
194    }
195
196    #[test]
197    fn getting_api_endpoint() {
198        let player_response_text = r#"==t.secret&&(e.secret=t.secret),userInfo&&"object"===_typeof(userInfo.info)&&(e.info=JSON.stringify(userInfo.info)),void 0!==window.advertTest&&(e.a_test=!0),!0===t.isUpdate&&(e.isUpdate=!0),$.ajax({type:"POST",url:atob("L2Z0b3I="),"#;
199        assert_eq!("/ftor", get_api_endpoint(player_response_text).unwrap());
200    }
201
202    #[test]
203    fn video_info_serializing() {
204        let video_info = VideoInfo::new("video", "060cab655974d46835b3f4405807acc2", "91873");
205
206        let serialized = serde_json::to_string(&video_info).unwrap();
207        assert_eq!(
208            r#"{"type":"video","hash":"060cab655974d46835b3f4405807acc2","id":"91873","bad_user":"True","info":"{}","cdn_is_working":"True"}"#,
209            serialized
210        );
211    }
212}