kodik_parser/
parser.rs

1use std::sync::{LazyLock, RwLock};
2
3use crate::decoder;
4use regex::Regex;
5use serde::Serialize;
6pub static VIDEO_INFO_ENDPOINT: RwLock<String> = RwLock::new(String::new());
7
8#[derive(Debug, Serialize, PartialEq, Eq)]
9pub struct VideoInfo<'a> {
10    #[serde(rename = "type")]
11    video_type: &'a str,
12    hash: &'a str,
13    id: &'a str,
14    bad_user: &'static str,
15    info: &'static str,
16    cdn_is_working: &'static str,
17}
18
19impl<'a> VideoInfo<'a> {
20    pub(crate) const fn new(video_type: &'a str, hash: &'a str, id: &'a str) -> Self {
21        Self {
22            video_type,
23            hash,
24            id,
25            bad_user: "True",
26            info: "{}",
27            cdn_is_working: "True",
28        }
29    }
30
31    fn iter(&'a self) -> std::array::IntoIter<(&'a str, &'a str), 6> {
32        [
33            ("type", self.video_type),
34            ("hash", self.hash),
35            ("id", self.id),
36            ("bad_user", self.bad_user),
37            ("info", self.info),
38            ("cdn_is_working", self.cdn_is_working),
39        ]
40        .into_iter()
41    }
42}
43
44impl<'a> IntoIterator for &'a VideoInfo<'a> {
45    type Item = (&'a str, &'a str);
46    type IntoIter = std::array::IntoIter<Self::Item, 6>;
47
48    fn into_iter(self) -> Self::IntoIter {
49        self.iter()
50    }
51}
52
53pub fn get_domain(url: &str) -> Result<&str, Box<dyn std::error::Error>> {
54    static DOMAIN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
55        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()
56    });
57
58    let domain = DOMAIN_REGEX.find(url).ok_or("No valid domain found")?;
59
60    Ok(domain.as_str())
61}
62
63pub fn extract_video_info(response_text: &'_ str) -> Result<VideoInfo<'_>, Box<dyn std::error::Error>> {
64    static TYPE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"videoInfo\.type = '(.*?)';").unwrap());
65    static HASH_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"videoInfo\.hash = '(.*?)';").unwrap());
66    static ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"videoInfo\.id = '(.*?)';").unwrap());
67
68    let video_type = TYPE_REGEX
69        .captures(response_text)
70        .ok_or("videoInfo.type not found")?
71        .get(1)
72        .unwrap()
73        .as_str();
74
75    let hash = HASH_REGEX
76        .captures(response_text)
77        .ok_or("videoInfo.hash not found")?
78        .get(1)
79        .unwrap()
80        .as_str();
81
82    let id = ID_REGEX
83        .captures(response_text)
84        .ok_or("videoInfo.id not found")?
85        .get(1)
86        .unwrap()
87        .as_str();
88
89    Ok(VideoInfo::new(video_type, hash, id))
90}
91
92pub fn extract_player_url(domain: &str, response_text: &str) -> Result<String, Box<dyn std::error::Error>> {
93    static PLAYER_PATH_REGEX: LazyLock<Regex> = LazyLock::new(|| {
94        Regex::new(r#"<script\s*type="text/javascript"\s*src="/(assets/js/app\.player_single[^"]*)""#).unwrap()
95    });
96
97    let player_path = PLAYER_PATH_REGEX
98        .captures(response_text)
99        .ok_or("There is no player path in response text")?
100        .get(1)
101        .unwrap()
102        .as_str();
103
104    Ok(format!("https://{domain}/{player_path}"))
105}
106
107pub fn get_api_endpoint(player_response_text: &str) -> Result<String, Box<dyn std::error::Error>> {
108    static ENDPOINT_REGEX: LazyLock<Regex> =
109        LazyLock::new(|| Regex::new(r#"\$\.ajax\([^>]+,url:\s*atob\(["\']([\w=]+)["\']\)"#).unwrap());
110
111    let encoded_api_endpoint = ENDPOINT_REGEX
112        .captures(player_response_text)
113        .ok_or("There is no api endpoint in player response")?
114        .get(1)
115        .unwrap()
116        .as_str();
117
118    decoder::b64(encoded_api_endpoint)
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_get_domain() {
127        let url_with_scheme = "https://kodik.info/seria/1484069/6a2e103e9acf9829c6cba7e69555afb1/720p";
128        let url_without_scheme = "kodik.info/seria/1484069/6a2e103e9acf9829c6cba7e69555afb1/720p";
129
130        assert_eq!("kodik.info", get_domain(url_with_scheme).unwrap());
131        assert_eq!("kodik.info", get_domain(url_without_scheme).unwrap());
132    }
133
134    #[test]
135    fn test_extract_video_info() {
136        let expected_video_info = VideoInfo::new("seria", "6a2e103e9acf9829c6cba7e69555afb1", "1484069");
137
138        let response_text = "
139  var videoInfo = {};
140   videoInfo.type = 'seria'; 
141   videoInfo.hash = '6a2e103e9acf9829c6cba7e69555afb1'; 
142   videoInfo.id = '1484069'; 
143</script>";
144
145        let video_info = extract_video_info(response_text).unwrap();
146
147        assert_eq!(expected_video_info, video_info);
148    }
149
150    #[test]
151    fn test_get_player_url() {
152        let domain = "kodik.info";
153        let response_text = r#"
154  </script>
155
156  <link rel="stylesheet" href="/assets/css/app.player.ffc43caed0b4bc0a9f41f95c06cd8230d49aaf7188dbba5f0770513420541101.css">
157  <script type="text/javascript" src="/assets/js/app.player_single.0a909e421830a88800354716d562e21654500844d220805110c7cf2092d70b05.js"></script>
158</head>
159<body class=" ">
160  <div class="main-box">
161    <style>
162  .resume-button { color: rgba(255, 255, 255, 0.75); }
163  .resume-button:hover { background-color: #171717; }
164  .resume-button { border-radius: 3px; }
165  .active-player .resume-button { border-radius: 3px; }"#;
166
167        let player_url = extract_player_url(domain, response_text).unwrap();
168        assert_eq!(
169            "https://kodik.info/assets/js/app.player_single.0a909e421830a88800354716d562e21654500844d220805110c7cf2092d70b05.js",
170            player_url
171        );
172    }
173
174    #[test]
175    fn test_get_api_endpoint() {
176        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="),"#;
177        assert_eq!("/ftor", get_api_endpoint(player_response_text).unwrap());
178    }
179}