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