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}