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}