1use super::database::LinkDatabase;
4use std::collections::HashMap;
5
6pub struct LinkStatistics<'a> {
8 database: &'a LinkDatabase,
9}
10
11impl<'a> LinkStatistics<'a> {
12 #[must_use]
14 pub const fn new(database: &'a LinkDatabase) -> Self {
15 Self { database }
16 }
17
18 #[must_use]
20 pub fn collect(&self) -> Statistics {
21 let all_links = self.database.all_links();
22 let total_links = all_links.len();
23
24 if total_links == 0 {
25 return Statistics::default();
26 }
27
28 let mut total_duration = 0.0;
29 let mut total_original_size = 0u64;
30 let mut total_proxy_size = 0u64;
31 let mut codec_distribution = HashMap::new();
32 let mut scale_distribution = HashMap::new();
33 let mut verified_count = 0;
34 let mut unverified_count = 0;
35
36 for link in &all_links {
37 total_duration += link.duration;
38
39 if let Ok(metadata) = std::fs::metadata(&link.original_path) {
41 total_original_size += metadata.len();
42 }
43 if let Ok(metadata) = std::fs::metadata(&link.proxy_path) {
44 total_proxy_size += metadata.len();
45 }
46
47 *codec_distribution.entry(link.codec.clone()).or_insert(0) += 1;
49
50 let scale_key = format!("{:.0}%", link.scale_factor * 100.0);
52 *scale_distribution.entry(scale_key).or_insert(0) += 1;
53
54 if link.verified_at.is_some() {
56 verified_count += 1;
57 } else {
58 unverified_count += 1;
59 }
60 }
61
62 let compression_ratio = if total_original_size > 0 {
63 total_proxy_size as f64 / total_original_size as f64
64 } else {
65 0.0
66 };
67
68 let space_saved = if total_original_size > total_proxy_size {
69 total_original_size - total_proxy_size
70 } else {
71 0
72 };
73
74 Statistics {
75 total_links,
76 total_duration,
77 total_original_size,
78 total_proxy_size,
79 compression_ratio,
80 space_saved,
81 codec_distribution,
82 scale_distribution,
83 verified_count,
84 unverified_count,
85 }
86 }
87
88 #[must_use]
90 pub fn codec_statistics(&self, codec: &str) -> CodecStatistics {
91 let all_links = self.database.all_links();
92 let codec_links: Vec<_> = all_links
93 .iter()
94 .filter(|link| link.codec == codec)
95 .collect();
96
97 let count = codec_links.len();
98 if count == 0 {
99 return CodecStatistics::default();
100 }
101
102 let mut total_size = 0u64;
103 let mut total_duration = 0.0;
104
105 for link in &codec_links {
106 if let Ok(metadata) = std::fs::metadata(&link.proxy_path) {
107 total_size += metadata.len();
108 }
109 total_duration += link.duration;
110 }
111
112 let avg_bitrate = if total_duration > 0.0 {
113 (total_size as f64 * 8.0 / total_duration) as u64
114 } else {
115 0
116 };
117
118 CodecStatistics {
119 codec: codec.to_string(),
120 count,
121 total_size,
122 total_duration,
123 avg_bitrate,
124 }
125 }
126
127 #[must_use]
129 pub fn popular_settings(&self) -> Vec<(f32, String, usize)> {
130 let all_links = self.database.all_links();
131 let mut settings_map: HashMap<(String, String), usize> = HashMap::new();
132
133 for link in &all_links {
134 let key = (format!("{:.2}", link.scale_factor), link.codec.clone());
135 *settings_map.entry(key).or_insert(0) += 1;
136 }
137
138 let mut settings_vec: Vec<_> = settings_map
139 .into_iter()
140 .map(|((scale, codec), count)| {
141 let scale_f32 = scale.parse::<f32>().unwrap_or(0.0);
142 (scale_f32, codec, count)
143 })
144 .collect();
145
146 settings_vec.sort_by(|a, b| b.2.cmp(&a.2));
147 settings_vec
148 }
149}
150
151#[derive(Debug, Clone, Default)]
153pub struct Statistics {
154 pub total_links: usize,
156
157 pub total_duration: f64,
159
160 pub total_original_size: u64,
162
163 pub total_proxy_size: u64,
165
166 pub compression_ratio: f64,
168
169 pub space_saved: u64,
171
172 pub codec_distribution: HashMap<String, usize>,
174
175 pub scale_distribution: HashMap<String, usize>,
177
178 pub verified_count: usize,
180
181 pub unverified_count: usize,
183}
184
185impl Statistics {
186 #[must_use]
188 pub fn summary(&self) -> String {
189 format!(
190 "Total Links: {}\n\
191 Total Duration: {:.2} hours\n\
192 Original Size: {}\n\
193 Proxy Size: {}\n\
194 Space Saved: {} ({:.1}%)\n\
195 Compression Ratio: {:.2}:1\n\
196 Verified: {} / Unverified: {}",
197 self.total_links,
198 self.total_duration / 3600.0,
199 format_bytes(self.total_original_size),
200 format_bytes(self.total_proxy_size),
201 format_bytes(self.space_saved),
202 (1.0 - self.compression_ratio) * 100.0,
203 1.0 / self.compression_ratio,
204 self.verified_count,
205 self.unverified_count
206 )
207 }
208
209 #[must_use]
211 pub fn most_used_codec(&self) -> Option<String> {
212 self.codec_distribution
213 .iter()
214 .max_by_key(|(_, count)| *count)
215 .map(|(codec, _)| codec.clone())
216 }
217
218 #[must_use]
220 pub fn verification_percentage(&self) -> f64 {
221 if self.total_links == 0 {
222 0.0
223 } else {
224 (self.verified_count as f64 / self.total_links as f64) * 100.0
225 }
226 }
227}
228
229#[derive(Debug, Clone, Default)]
231pub struct CodecStatistics {
232 pub codec: String,
234
235 pub count: usize,
237
238 pub total_size: u64,
240
241 pub total_duration: f64,
243
244 pub avg_bitrate: u64,
246}
247
248fn format_bytes(bytes: u64) -> String {
249 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
250 let mut size = bytes as f64;
251 let mut unit_index = 0;
252
253 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
254 size /= 1024.0;
255 unit_index += 1;
256 }
257
258 format!("{:.2} {}", size, UNITS[unit_index])
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::link::database::ProxyLinkRecord;
265 use std::path::PathBuf;
266
267 #[tokio::test]
268 async fn test_statistics_collection() {
269 let temp_dir = std::env::temp_dir();
270 let db_path = temp_dir.join("test_stats.json");
271
272 let mut db = LinkDatabase::new(&db_path)
273 .await
274 .expect("should succeed in test");
275
276 let record = ProxyLinkRecord {
277 proxy_path: PathBuf::from("proxy.mp4"),
278 original_path: PathBuf::from("original.mov"),
279 scale_factor: 0.25,
280 codec: "h264".to_string(),
281 duration: 60.0,
282 timecode: None,
283 created_at: 123456789,
284 verified_at: Some(123456800),
285 metadata: HashMap::new(),
286 };
287
288 db.add_link(record).expect("should succeed in test");
289
290 let stats_collector = LinkStatistics::new(&db);
291 let stats = stats_collector.collect();
292
293 assert_eq!(stats.total_links, 1);
294 assert_eq!(stats.total_duration, 60.0);
295 assert_eq!(stats.verified_count, 1);
296 assert_eq!(stats.unverified_count, 0);
297
298 let _ = std::fs::remove_file(db_path);
300 }
301
302 #[test]
303 fn test_format_bytes() {
304 assert_eq!(format_bytes(500), "500.00 B");
305 assert_eq!(format_bytes(1024), "1.00 KB");
306 assert_eq!(format_bytes(1_048_576), "1.00 MB");
307 assert_eq!(format_bytes(1_073_741_824), "1.00 GB");
308 }
309
310 #[test]
311 fn test_statistics_summary() {
312 let stats = Statistics {
313 total_links: 10,
314 total_duration: 600.0,
315 total_original_size: 1_000_000_000,
316 total_proxy_size: 100_000_000,
317 compression_ratio: 0.1,
318 space_saved: 900_000_000,
319 codec_distribution: HashMap::new(),
320 scale_distribution: HashMap::new(),
321 verified_count: 8,
322 unverified_count: 2,
323 };
324
325 let summary = stats.summary();
326 assert!(summary.contains("Total Links: 10"));
327 assert!(summary.contains("Verified: 8"));
328 }
329
330 #[test]
331 fn test_verification_percentage() {
332 let stats = Statistics {
333 total_links: 10,
334 verified_count: 7,
335 ..Default::default()
336 };
337
338 assert_eq!(stats.verification_percentage(), 70.0);
339 }
340}