1#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq)]
12pub struct ProxyEntry {
13 pub original_path: String,
15 pub proxy_path: String,
17 pub width: u32,
19 pub height: u32,
21 pub bitrate_kbps: u32,
23 pub codec: Option<String>,
25}
26
27impl ProxyEntry {
28 pub fn new(
30 original_path: impl Into<String>,
31 proxy_path: impl Into<String>,
32 width: u32,
33 height: u32,
34 bitrate_kbps: u32,
35 ) -> Self {
36 Self {
37 original_path: original_path.into(),
38 proxy_path: proxy_path.into(),
39 width,
40 height,
41 bitrate_kbps,
42 codec: None,
43 }
44 }
45
46 pub fn is_valid(&self) -> bool {
48 !self.original_path.is_empty()
49 && !self.proxy_path.is_empty()
50 && self.width > 0
51 && self.height > 0
52 && self.bitrate_kbps > 0
53 }
54
55 pub fn display_label(&self) -> String {
57 format!("{}x{}@{}kbps", self.width, self.height, self.bitrate_kbps)
58 }
59
60 pub fn pixel_count(&self) -> u64 {
62 u64::from(self.width) * u64::from(self.height)
63 }
64}
65
66#[derive(Debug, Default)]
68pub struct ProxyIndex {
69 map: HashMap<String, Vec<ProxyEntry>>,
71}
72
73impl ProxyIndex {
74 pub fn new() -> Self {
76 Self::default()
77 }
78
79 pub fn insert(&mut self, entry: ProxyEntry) {
81 self.map
82 .entry(entry.original_path.clone())
83 .or_default()
84 .push(entry);
85 }
86
87 pub fn find_by_original(&self, original_path: &str) -> &[ProxyEntry] {
89 self.map
90 .get(original_path)
91 .map(Vec::as_slice)
92 .unwrap_or(&[])
93 }
94
95 pub fn remove(&mut self, original_path: &str) -> Vec<ProxyEntry> {
97 self.map.remove(original_path).unwrap_or_default()
98 }
99
100 pub fn count(&self) -> usize {
102 self.map.values().map(Vec::len).sum()
103 }
104
105 pub fn original_count(&self) -> usize {
107 self.map.len()
108 }
109
110 pub fn is_empty(&self) -> bool {
112 self.map.is_empty()
113 }
114
115 pub fn all_entries(&self) -> impl Iterator<Item = &ProxyEntry> {
117 self.map.values().flat_map(|v| v.iter())
118 }
119
120 pub fn contains(&self, original_path: &str) -> bool {
122 self.map.contains_key(original_path)
123 }
124
125 pub fn best_quality(&self, original_path: &str) -> Option<&ProxyEntry> {
127 self.find_by_original(original_path)
128 .iter()
129 .max_by_key(|e| e.bitrate_kbps)
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 fn make_entry(orig: &str, proxy: &str, w: u32, h: u32, br: u32) -> ProxyEntry {
138 ProxyEntry::new(orig, proxy, w, h, br)
139 }
140
141 #[test]
142 fn test_entry_is_valid() {
143 let e = make_entry("/media/orig.mov", "/proxy/p.mp4", 640, 360, 500);
144 assert!(e.is_valid());
145 }
146
147 #[test]
148 fn test_entry_invalid_empty_path() {
149 let e = make_entry("", "/proxy/p.mp4", 640, 360, 500);
150 assert!(!e.is_valid());
151 }
152
153 #[test]
154 fn test_entry_invalid_zero_dimension() {
155 let e = make_entry("/media/orig.mov", "/proxy/p.mp4", 0, 360, 500);
156 assert!(!e.is_valid());
157 }
158
159 #[test]
160 fn test_entry_invalid_zero_bitrate() {
161 let e = make_entry("/media/orig.mov", "/proxy/p.mp4", 640, 360, 0);
162 assert!(!e.is_valid());
163 }
164
165 #[test]
166 fn test_entry_display_label() {
167 let e = make_entry("/orig.mov", "/p.mp4", 1280, 720, 2000);
168 assert_eq!(e.display_label(), "1280x720@2000kbps");
169 }
170
171 #[test]
172 fn test_entry_pixel_count() {
173 let e = make_entry("/orig.mov", "/p.mp4", 1920, 1080, 8000);
174 assert_eq!(e.pixel_count(), 1920 * 1080);
175 }
176
177 #[test]
178 fn test_index_insert_and_count() {
179 let mut idx = ProxyIndex::new();
180 idx.insert(make_entry("/orig.mov", "/p1.mp4", 640, 360, 500));
181 idx.insert(make_entry("/orig.mov", "/p2.mp4", 1280, 720, 2000));
182 assert_eq!(idx.count(), 2);
183 assert_eq!(idx.original_count(), 1);
184 }
185
186 #[test]
187 fn test_index_find_by_original() {
188 let mut idx = ProxyIndex::new();
189 idx.insert(make_entry("/orig.mov", "/p.mp4", 640, 360, 500));
190 let found = idx.find_by_original("/orig.mov");
191 assert_eq!(found.len(), 1);
192 assert_eq!(found[0].proxy_path, "/p.mp4");
193 }
194
195 #[test]
196 fn test_index_find_by_original_not_found() {
197 let idx = ProxyIndex::new();
198 assert!(idx.find_by_original("/missing.mov").is_empty());
199 }
200
201 #[test]
202 fn test_index_remove() {
203 let mut idx = ProxyIndex::new();
204 idx.insert(make_entry("/orig.mov", "/p.mp4", 640, 360, 500));
205 let removed = idx.remove("/orig.mov");
206 assert_eq!(removed.len(), 1);
207 assert_eq!(idx.count(), 0);
208 }
209
210 #[test]
211 fn test_index_contains() {
212 let mut idx = ProxyIndex::new();
213 idx.insert(make_entry("/orig.mov", "/p.mp4", 640, 360, 500));
214 assert!(idx.contains("/orig.mov"));
215 assert!(!idx.contains("/other.mov"));
216 }
217
218 #[test]
219 fn test_index_best_quality() {
220 let mut idx = ProxyIndex::new();
221 idx.insert(make_entry("/orig.mov", "/p_draft.mp4", 640, 360, 500));
222 idx.insert(make_entry("/orig.mov", "/p_delivery.mp4", 1920, 1080, 8000));
223 let best = idx
224 .best_quality("/orig.mov")
225 .expect("should succeed in test");
226 assert_eq!(best.proxy_path, "/p_delivery.mp4");
227 }
228
229 #[test]
230 fn test_index_is_empty() {
231 let idx = ProxyIndex::new();
232 assert!(idx.is_empty());
233 }
234}