1use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ProxySpec {
13 pub width: u32,
15 pub height: u32,
17 pub codec: String,
19 pub bitrate: u32,
21}
22
23impl ProxySpec {
24 #[must_use]
26 pub fn new(width: u32, height: u32, codec: impl Into<String>, bitrate: u32) -> Self {
27 Self {
28 width,
29 height,
30 codec: codec.into(),
31 bitrate,
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct ProxyMetadata {
39 pub original_path: String,
41 pub proxy_path: String,
43 pub spec: ProxySpec,
45 pub created_at: u64,
47 pub checksum: String,
49}
50
51impl ProxyMetadata {
52 #[must_use]
54 pub fn new(
55 original_path: impl Into<String>,
56 proxy_path: impl Into<String>,
57 spec: ProxySpec,
58 created_at: u64,
59 checksum: impl Into<String>,
60 ) -> Self {
61 Self {
62 original_path: original_path.into(),
63 proxy_path: proxy_path.into(),
64 spec,
65 created_at,
66 checksum: checksum.into(),
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum ProxyValidationError {
74 DimensionMismatch {
76 expected: (u32, u32),
78 actual: (u32, u32),
80 },
81 ChecksumMismatch,
83 PathNotFound(String),
85 NotRegistered(String),
87 EmptyPath,
89}
90
91impl std::fmt::Display for ProxyValidationError {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 Self::DimensionMismatch { expected, actual } => write!(
95 f,
96 "dimension mismatch: expected {}x{}, got {}x{}",
97 expected.0, expected.1, actual.0, actual.1
98 ),
99 Self::ChecksumMismatch => write!(f, "proxy checksum mismatch"),
100 Self::PathNotFound(p) => write!(f, "path not found: {p}"),
101 Self::NotRegistered(p) => write!(f, "no proxy registered for: {p}"),
102 Self::EmptyPath => write!(f, "path must not be empty"),
103 }
104 }
105}
106
107impl std::error::Error for ProxyValidationError {}
108
109#[derive(Debug, Clone, Default)]
115pub struct ProxyManagerSpec {
116 records: HashMap<String, ProxyMetadata>,
117}
118
119impl ProxyManagerSpec {
120 #[must_use]
122 pub fn new() -> Self {
123 Self {
124 records: HashMap::new(),
125 }
126 }
127
128 pub fn register(&mut self, meta: ProxyMetadata) -> Result<(), ProxyValidationError> {
134 if meta.original_path.is_empty() || meta.proxy_path.is_empty() {
135 return Err(ProxyValidationError::EmptyPath);
136 }
137 self.records.insert(meta.original_path.clone(), meta);
138 Ok(())
139 }
140
141 #[must_use]
143 pub fn find_proxy(&self, original_path: &str) -> Option<&ProxyMetadata> {
144 self.records.get(original_path)
145 }
146
147 pub fn is_proxy_valid(
157 &self,
158 original_path: &str,
159 actual_width: u32,
160 actual_height: u32,
161 ) -> Result<bool, ProxyValidationError> {
162 let meta = self
163 .records
164 .get(original_path)
165 .ok_or_else(|| ProxyValidationError::NotRegistered(original_path.to_string()))?;
166
167 if meta.spec.width == actual_width && meta.spec.height == actual_height {
168 Ok(true)
169 } else {
170 Ok(false)
171 }
172 }
173
174 pub fn remove(&mut self, original_path: &str) -> Option<ProxyMetadata> {
176 self.records.remove(original_path)
177 }
178
179 #[must_use]
181 pub fn list_all(&self) -> Vec<&ProxyMetadata> {
182 self.records.values().collect()
183 }
184
185 #[must_use]
187 pub fn count(&self) -> usize {
188 self.records.len()
189 }
190}
191
192#[cfg(test)]
197mod tests {
198 use super::*;
199
200 fn make_spec(w: u32, h: u32) -> ProxySpec {
201 ProxySpec::new(w, h, "h264", 2000)
202 }
203
204 fn make_meta(orig: &str, proxy: &str, w: u32, h: u32) -> ProxyMetadata {
205 ProxyMetadata::new(orig, proxy, make_spec(w, h), 1_700_000_000, "deadbeef")
206 }
207
208 #[test]
209 fn test_register_and_find() {
210 let mut mgr = ProxyManagerSpec::new();
211 let meta = make_meta("/orig/clip.mov", "/proxy/clip_proxy.mp4", 1280, 720);
212 mgr.register(meta).expect("register should succeed");
213
214 let found = mgr.find_proxy("/orig/clip.mov").expect("should find proxy");
215 assert_eq!(found.proxy_path, "/proxy/clip_proxy.mp4");
216 }
217
218 #[test]
219 fn test_find_missing_returns_none() {
220 let mgr = ProxyManagerSpec::new();
221 assert!(mgr.find_proxy("/nonexistent.mov").is_none());
222 }
223
224 #[test]
225 fn test_register_empty_original_path_errors() {
226 let mut mgr = ProxyManagerSpec::new();
227 let meta = make_meta("", "/proxy/clip.mp4", 1280, 720);
228 let err = mgr.register(meta).unwrap_err();
229 assert_eq!(err, ProxyValidationError::EmptyPath);
230 }
231
232 #[test]
233 fn test_register_empty_proxy_path_errors() {
234 let mut mgr = ProxyManagerSpec::new();
235 let meta = make_meta("/orig/clip.mov", "", 1280, 720);
236 let err = mgr.register(meta).unwrap_err();
237 assert_eq!(err, ProxyValidationError::EmptyPath);
238 }
239
240 #[test]
241 fn test_is_proxy_valid_dimensions_match() {
242 let mut mgr = ProxyManagerSpec::new();
243 mgr.register(make_meta("/orig/a.mov", "/proxy/a.mp4", 1280, 720))
244 .expect("register");
245 assert_eq!(mgr.is_proxy_valid("/orig/a.mov", 1280, 720), Ok(true));
246 }
247
248 #[test]
249 fn test_is_proxy_valid_dimension_mismatch() {
250 let mut mgr = ProxyManagerSpec::new();
251 mgr.register(make_meta("/orig/a.mov", "/proxy/a.mp4", 1280, 720))
252 .expect("register");
253 assert_eq!(mgr.is_proxy_valid("/orig/a.mov", 640, 360), Ok(false));
254 }
255
256 #[test]
257 fn test_is_proxy_valid_not_registered() {
258 let mgr = ProxyManagerSpec::new();
259 let err = mgr.is_proxy_valid("/unknown.mov", 1280, 720).unwrap_err();
260 assert!(matches!(err, ProxyValidationError::NotRegistered(_)));
261 }
262
263 #[test]
264 fn test_remove_existing() {
265 let mut mgr = ProxyManagerSpec::new();
266 mgr.register(make_meta("/orig/b.mov", "/proxy/b.mp4", 1920, 1080))
267 .expect("register");
268 let removed = mgr.remove("/orig/b.mov");
269 assert!(removed.is_some());
270 assert!(mgr.find_proxy("/orig/b.mov").is_none());
271 }
272
273 #[test]
274 fn test_remove_missing_returns_none() {
275 let mut mgr = ProxyManagerSpec::new();
276 assert!(mgr.remove("/nonexistent.mov").is_none());
277 }
278
279 #[test]
280 fn test_count() {
281 let mut mgr = ProxyManagerSpec::new();
282 assert_eq!(mgr.count(), 0);
283 mgr.register(make_meta("/orig/c.mov", "/proxy/c.mp4", 1280, 720))
284 .expect("register");
285 assert_eq!(mgr.count(), 1);
286 mgr.register(make_meta("/orig/d.mov", "/proxy/d.mp4", 1920, 1080))
287 .expect("register");
288 assert_eq!(mgr.count(), 2);
289 }
290
291 #[test]
292 fn test_list_all() {
293 let mut mgr = ProxyManagerSpec::new();
294 mgr.register(make_meta("/orig/e.mov", "/proxy/e.mp4", 1280, 720))
295 .expect("register");
296 mgr.register(make_meta("/orig/f.mov", "/proxy/f.mp4", 1920, 1080))
297 .expect("register");
298 let list = mgr.list_all();
299 assert_eq!(list.len(), 2);
300 }
301
302 #[test]
303 fn test_register_overwrites_duplicate() {
304 let mut mgr = ProxyManagerSpec::new();
305 mgr.register(make_meta("/orig/g.mov", "/proxy/g_v1.mp4", 1280, 720))
306 .expect("register v1");
307 mgr.register(make_meta("/orig/g.mov", "/proxy/g_v2.mp4", 1920, 1080))
308 .expect("register v2");
309 assert_eq!(mgr.count(), 1);
310 let found = mgr.find_proxy("/orig/g.mov").expect("should find");
311 assert_eq!(found.proxy_path, "/proxy/g_v2.mp4");
312 assert_eq!(found.spec.width, 1920);
313 }
314
315 #[test]
316 fn test_proxy_spec_fields() {
317 let spec = ProxySpec::new(3840, 2160, "prores_proxy", 8000);
318 assert_eq!(spec.width, 3840);
319 assert_eq!(spec.height, 2160);
320 assert_eq!(spec.codec, "prores_proxy");
321 assert_eq!(spec.bitrate, 8000);
322 }
323
324 #[test]
325 fn test_proxy_metadata_fields() {
326 let meta = ProxyMetadata::new(
327 "/orig/h.mxf",
328 "/proxy/h_proxy.mp4",
329 ProxySpec::new(960, 540, "h264", 1500),
330 1_234_567_890,
331 "abc123",
332 );
333 assert_eq!(meta.original_path, "/orig/h.mxf");
334 assert_eq!(meta.proxy_path, "/proxy/h_proxy.mp4");
335 assert_eq!(meta.created_at, 1_234_567_890);
336 assert_eq!(meta.checksum, "abc123");
337 }
338
339 #[test]
340 fn test_validation_error_display_dimension_mismatch() {
341 let err = ProxyValidationError::DimensionMismatch {
342 expected: (1280, 720),
343 actual: (640, 360),
344 };
345 let msg = err.to_string();
346 assert!(msg.contains("1280x720"));
347 assert!(msg.contains("640x360"));
348 }
349
350 #[test]
351 fn test_validation_error_display_not_registered() {
352 let err = ProxyValidationError::NotRegistered("/missing.mov".to_string());
353 assert!(err.to_string().contains("/missing.mov"));
354 }
355}