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}