Skip to main content

oximedia_proxy/link/
statistics.rs

1//! Link statistics and analytics.
2
3use super::database::LinkDatabase;
4use std::collections::HashMap;
5
6/// Link statistics collector.
7pub struct LinkStatistics<'a> {
8    database: &'a LinkDatabase,
9}
10
11impl<'a> LinkStatistics<'a> {
12    /// Create a new statistics collector.
13    #[must_use]
14    pub const fn new(database: &'a LinkDatabase) -> Self {
15        Self { database }
16    }
17
18    /// Get comprehensive statistics about all links.
19    #[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            // Get file sizes if files exist
40            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
48            *codec_distribution.entry(link.codec.clone()).or_insert(0) += 1;
49
50            // Scale factor distribution
51            let scale_key = format!("{:.0}%", link.scale_factor * 100.0);
52            *scale_distribution.entry(scale_key).or_insert(0) += 1;
53
54            // Verification status
55            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    /// Get statistics for a specific codec.
89    #[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    /// Get the most popular proxy settings.
128    #[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/// Comprehensive link statistics.
152#[derive(Debug, Clone, Default)]
153pub struct Statistics {
154    /// Total number of links.
155    pub total_links: usize,
156
157    /// Total duration of all media in seconds.
158    pub total_duration: f64,
159
160    /// Total size of original files in bytes.
161    pub total_original_size: u64,
162
163    /// Total size of proxy files in bytes.
164    pub total_proxy_size: u64,
165
166    /// Average compression ratio (proxy size / original size).
167    pub compression_ratio: f64,
168
169    /// Total space saved in bytes.
170    pub space_saved: u64,
171
172    /// Distribution of codecs used.
173    pub codec_distribution: HashMap<String, usize>,
174
175    /// Distribution of scale factors.
176    pub scale_distribution: HashMap<String, usize>,
177
178    /// Number of verified links.
179    pub verified_count: usize,
180
181    /// Number of unverified links.
182    pub unverified_count: usize,
183}
184
185impl Statistics {
186    /// Get a human-readable summary.
187    #[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    /// Get the most used codec.
210    #[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    /// Get verification percentage.
219    #[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/// Codec-specific statistics.
230#[derive(Debug, Clone, Default)]
231pub struct CodecStatistics {
232    /// Codec name.
233    pub codec: String,
234
235    /// Number of proxies using this codec.
236    pub count: usize,
237
238    /// Total size of all proxies with this codec.
239    pub total_size: u64,
240
241    /// Total duration of all proxies with this codec.
242    pub total_duration: f64,
243
244    /// Average bitrate in bits per second.
245    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        // Clean up
299        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}