libsubconverter/parser/
infoparser.rs1use std::time::{Duration, UNIX_EPOCH};
2
3use crate::models::{Proxy, RegexMatchConfigs};
4use crate::utils::base64::url_safe_base64_decode;
5use crate::utils::system::safe_system_time;
6use crate::utils::url::get_url_arg;
7use regex::Regex;
8
9pub fn stream_to_int(stream: &str) -> u64 {
11 if stream.is_empty() {
12 return 0;
13 }
14
15 let units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
16 let mut index = units.len() - 1;
17
18 loop {
19 if stream.ends_with(units[index]) {
20 let value_str = stream.trim_end_matches(units[index]);
21 let base_value = value_str.parse::<f64>().unwrap_or(0.0);
22 return (base_value * (1024_f64.powi(index as i32))) as u64;
23 }
24
25 if index == 0 {
26 break;
27 }
28 index -= 1;
29 }
30
31 stream.parse::<u64>().unwrap_or(0)
33}
34
35fn percent_to_double(percent: &str) -> f64 {
37 if percent.ends_with('%') {
38 let value_str = &percent[..percent.len() - 1];
39 return value_str.parse::<f64>().unwrap_or(0.0) / 100.0;
40 }
41 0.0
42}
43
44pub fn date_string_to_timestamp(date: &str) -> u64 {
46 let now = safe_system_time()
47 .duration_since(UNIX_EPOCH)
48 .unwrap_or_else(|_| Duration::from_secs(0))
49 .as_secs();
50
51 if date.starts_with("left=") {
52 let mut seconds_left = 0;
53 let time_str = &date[5..];
54
55 if time_str.ends_with('d') {
56 let days = time_str[..time_str.len() - 1].parse::<f64>().unwrap_or(0.0);
57 seconds_left = (days * 86400.0) as u64;
58 }
59
60 return now + seconds_left;
61 } else {
62 let parts: Vec<&str> = date.split(':').collect();
63 if parts.len() != 6 {
64 return 0;
65 }
66
67 let _year = parts[0].parse::<i32>().unwrap_or(1900);
70 let _month = parts[1].parse::<u32>().unwrap_or(1);
71 let _day = parts[2].parse::<u32>().unwrap_or(1);
72 let _hour = parts[3].parse::<u32>().unwrap_or(0);
73 let _minute = parts[4].parse::<u32>().unwrap_or(0);
74 let _second = parts[5].parse::<u32>().unwrap_or(0);
75
76 return now;
79 }
80}
81
82pub fn get_sub_info_from_header(header: &str) -> Option<String> {
84 let re = Regex::new(r"(?i)^Subscription-UserInfo: (.*?)$").ok()?;
85
86 if let Some(captures) = re.captures(header) {
87 if let Some(matched) = captures.get(1) {
88 let ret_str = matched.as_str().trim();
89 if !ret_str.is_empty() {
90 return Some(ret_str.to_string());
91 }
92 }
93 }
94
95 None
96}
97
98pub fn get_sub_info_from_nodes(
100 nodes: &[Proxy],
101 stream_rules: &RegexMatchConfigs,
102 time_rules: &RegexMatchConfigs,
103) -> Option<String> {
104 let mut stream_info = String::new();
105 let mut time_info = String::new();
106
107 for node in nodes {
108 let remarks = &node.remark;
109
110 if stream_info.is_empty() {
112 for rule in stream_rules {
113 let re = Regex::new(&rule._match).ok()?;
114 if re.is_match(remarks) {
115 let new_remark = re.replace(remarks, &rule.replace).to_string();
116 if new_remark != *remarks {
117 stream_info = new_remark;
118 break;
119 }
120 }
121 }
122 }
123
124 if time_info.is_empty() {
126 for rule in time_rules {
127 let re = Regex::new(&rule._match).ok()?;
128 if re.is_match(remarks) {
129 let new_remark = re.replace(remarks, &rule.replace).to_string();
130 if new_remark != *remarks {
131 time_info = new_remark;
132 break;
133 }
134 }
135 }
136 }
137
138 if !stream_info.is_empty() && !time_info.is_empty() {
139 break;
140 }
141 }
142
143 if stream_info.is_empty() && time_info.is_empty() {
144 return None;
145 }
146
147 let mut total: u64 = 0;
149 let mut used: u64 = 0;
150
151 let total_str = get_url_arg(&stream_info, "total");
152 let left_str = get_url_arg(&stream_info, "left");
153 let used_str = get_url_arg(&stream_info, "used");
154
155 if total_str.contains('%') {
156 if !used_str.is_empty() {
157 used = stream_to_int(&used_str);
158 let percentage = percent_to_double(&total_str);
159 if percentage > 0.0 {
160 total = (used as f64 / (1.0 - percentage)) as u64;
161 }
162 } else if !left_str.is_empty() {
163 let left = stream_to_int(&left_str);
164 let percentage = percent_to_double(&total_str);
165 if percentage > 0.0 {
166 total = (left as f64 / percentage) as u64;
167 if left > total {
168 used = 0;
169 } else {
170 used = total - left;
171 }
172 }
173 }
174 } else {
175 total = stream_to_int(&total_str);
176 if !used_str.is_empty() {
177 used = stream_to_int(&used_str);
178 } else if !left_str.is_empty() {
179 let left = stream_to_int(&left_str);
180 if left > total {
181 used = 0;
182 } else {
183 used = total - left;
184 }
185 }
186 }
187
188 let mut result = format!("upload=0; download={}; total={};", used, total);
189
190 let expire = date_string_to_timestamp(&time_info);
192 if expire > 0 {
193 result.push_str(&format!(" expire={};", expire));
194 }
195
196 Some(result)
197}
198
199pub fn get_sub_info_from_ssd(sub: &str) -> Option<String> {
201 if !sub.starts_with("ssd://") {
202 return None;
203 }
204
205 let decoded = url_safe_base64_decode(&sub[6..]);
206
207 let json: serde_json::Value = match serde_json::from_str(&decoded) {
209 Ok(val) => val,
210 Err(_) => return None,
211 };
212
213 let used_str = json.get("traffic_used")?.as_str()?;
214 let total_str = json.get("traffic_total")?.as_str()?;
215
216 let gb_to_bytes = 1024u64.pow(3);
218 let used = used_str.parse::<f64>().unwrap_or(0.0) * gb_to_bytes as f64;
219 let total = total_str.parse::<f64>().unwrap_or(0.0) * gb_to_bytes as f64;
220
221 let mut result = format!(
222 "upload=0; download={}; total={};",
223 used as u64, total as u64
224 );
225
226 if let Some(expire_str) = json.get("expiry").and_then(|v| v.as_str()) {
227 let re = Regex::new(r"(\d+)-(\d+)-(\d+) (.*)").unwrap();
229 let formatted_date = re.replace(expire_str, "$1:$2:$3:$4").to_string();
230
231 let expire = date_string_to_timestamp(&formatted_date);
232 if expire > 0 {
233 result.push_str(&format!(" expire={};", expire));
234 }
235 }
236
237 Some(result)
238}