1#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum OfflineStatus {
15 Available,
17 Missing,
19 Generating,
21 Reconnected,
23 Substituted,
25}
26
27#[derive(Debug, Clone)]
29#[allow(dead_code)]
30pub struct OfflineProxyClip {
31 pub id: String,
33 pub proxy_path: PathBuf,
35 pub original_path: Option<PathBuf>,
37 pub status: OfflineStatus,
39 pub resolution_fraction: f32,
41}
42
43impl OfflineProxyClip {
44 #[must_use]
46 pub fn new(id: impl Into<String>, proxy_path: impl Into<PathBuf>) -> Self {
47 Self {
48 id: id.into(),
49 proxy_path: proxy_path.into(),
50 original_path: None,
51 status: OfflineStatus::Available,
52 resolution_fraction: 0.25,
53 }
54 }
55
56 #[must_use]
58 pub fn with_original(mut self, original: impl Into<PathBuf>) -> Self {
59 self.original_path = Some(original.into());
60 self
61 }
62
63 #[must_use]
65 pub fn with_resolution_fraction(mut self, fraction: f32) -> Self {
66 self.resolution_fraction = fraction.clamp(0.0, 1.0);
67 self
68 }
69
70 #[must_use]
72 pub fn proxy_exists(&self) -> bool {
73 self.proxy_path.exists()
74 }
75
76 #[must_use]
78 pub fn is_reconnected(&self) -> bool {
79 self.status == OfflineStatus::Reconnected && self.original_path.is_some()
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum SubstitutionStrategy {
86 BlackFrame,
88 Placeholder,
90 Skip,
92 AutoRegenerate,
94}
95
96impl Default for SubstitutionStrategy {
97 fn default() -> Self {
98 Self::Placeholder
99 }
100}
101
102#[allow(dead_code)]
104pub struct OfflineProxySession {
105 clips: HashMap<String, OfflineProxyClip>,
107 substitution_strategy: SubstitutionStrategy,
109 strict_mode: bool,
111}
112
113impl OfflineProxySession {
114 #[must_use]
116 pub fn new() -> Self {
117 Self {
118 clips: HashMap::new(),
119 substitution_strategy: SubstitutionStrategy::default(),
120 strict_mode: false,
121 }
122 }
123
124 #[must_use]
126 pub fn with_strategy(mut self, strategy: SubstitutionStrategy) -> Self {
127 self.substitution_strategy = strategy;
128 self
129 }
130
131 #[must_use]
133 pub fn strict(mut self) -> Self {
134 self.strict_mode = true;
135 self
136 }
137
138 pub fn register(&mut self, clip: OfflineProxyClip) {
140 self.clips.insert(clip.id.clone(), clip);
141 }
142
143 #[must_use]
145 pub fn get(&self, id: &str) -> Option<&OfflineProxyClip> {
146 self.clips.get(id)
147 }
148
149 pub fn get_mut(&mut self, id: &str) -> Option<&mut OfflineProxyClip> {
151 self.clips.get_mut(id)
152 }
153
154 #[must_use]
156 pub fn clip_count(&self) -> usize {
157 self.clips.len()
158 }
159
160 #[must_use]
162 pub fn count_by_status(&self, status: &OfflineStatus) -> usize {
163 self.clips.values().filter(|c| &c.status == status).count()
164 }
165
166 pub fn reconnect(&mut self, id: &str, original_path: impl Into<PathBuf>) -> bool {
170 if let Some(clip) = self.clips.get_mut(id) {
171 clip.original_path = Some(original_path.into());
172 clip.status = OfflineStatus::Reconnected;
173 true
174 } else {
175 false
176 }
177 }
178
179 pub fn substitute(&mut self, id: &str) -> bool {
181 if let Some(clip) = self.clips.get_mut(id) {
182 clip.status = OfflineStatus::Substituted;
183 true
184 } else {
185 false
186 }
187 }
188
189 #[must_use]
191 pub fn substitution_strategy(&self) -> SubstitutionStrategy {
192 self.substitution_strategy
193 }
194
195 #[must_use]
197 pub fn is_strict(&self) -> bool {
198 self.strict_mode
199 }
200
201 #[must_use]
203 pub fn clips_needing_reconnection(&self) -> Vec<&OfflineProxyClip> {
204 self.clips
205 .values()
206 .filter(|c| c.original_path.is_none())
207 .collect()
208 }
209
210 #[must_use]
212 pub fn reconnected_clips(&self) -> Vec<&OfflineProxyClip> {
213 self.clips.values().filter(|c| c.is_reconnected()).collect()
214 }
215}
216
217impl Default for OfflineProxySession {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223#[derive(Debug, Default)]
225#[allow(dead_code)]
226pub struct ReconnectResult {
227 pub reconnected: usize,
229 pub failed: usize,
231 pub missing_originals: Vec<PathBuf>,
233}
234
235impl ReconnectResult {
236 #[must_use]
238 pub fn new() -> Self {
239 Self::default()
240 }
241
242 #[must_use]
244 pub fn total(&self) -> usize {
245 self.reconnected + self.failed
246 }
247
248 #[must_use]
250 pub fn success_rate(&self) -> f32 {
251 let total = self.total();
252 if total == 0 {
253 return 1.0;
254 }
255 self.reconnected as f32 / total as f32
256 }
257}
258
259#[allow(dead_code)]
261pub struct AutoReconnector {
262 search_root: PathBuf,
264 extensions: Vec<String>,
266}
267
268impl AutoReconnector {
269 #[must_use]
271 pub fn new(search_root: impl Into<PathBuf>) -> Self {
272 Self {
273 search_root: search_root.into(),
274 extensions: vec![
275 "mov".to_string(),
276 "mxf".to_string(),
277 "mp4".to_string(),
278 "r3d".to_string(),
279 "braw".to_string(),
280 ],
281 }
282 }
283
284 #[must_use]
286 pub fn with_extensions(mut self, exts: Vec<String>) -> Self {
287 self.extensions = exts;
288 self
289 }
290
291 #[must_use]
293 pub fn search_root(&self) -> &Path {
294 &self.search_root
295 }
296
297 #[must_use]
299 pub fn includes_extension(&self, ext: &str) -> bool {
300 self.extensions.iter().any(|e| e.eq_ignore_ascii_case(ext))
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_offline_proxy_clip_new() {
310 let clip = OfflineProxyClip::new("clip001", "/proxy/clip001.mp4");
311 assert_eq!(clip.id, "clip001");
312 assert_eq!(clip.status, OfflineStatus::Available);
313 assert!(clip.original_path.is_none());
314 }
315
316 #[test]
317 fn test_offline_proxy_clip_with_original() {
318 let clip = OfflineProxyClip::new("clip001", "/proxy/clip001.mp4")
319 .with_original("/original/clip001.mov");
320 assert!(clip.original_path.is_some());
321 }
322
323 #[test]
324 fn test_offline_proxy_clip_resolution_fraction_clamp() {
325 let clip = OfflineProxyClip::new("c1", "/p.mp4").with_resolution_fraction(2.0);
326 assert_eq!(clip.resolution_fraction, 1.0);
327
328 let clip2 = OfflineProxyClip::new("c2", "/p.mp4").with_resolution_fraction(-0.5);
329 assert_eq!(clip2.resolution_fraction, 0.0);
330 }
331
332 #[test]
333 fn test_offline_proxy_clip_is_reconnected_false_without_original() {
334 let mut clip = OfflineProxyClip::new("c1", "/p.mp4");
335 clip.status = OfflineStatus::Reconnected;
336 assert!(!clip.is_reconnected());
338 }
339
340 #[test]
341 fn test_offline_proxy_clip_is_reconnected_true() {
342 let mut clip = OfflineProxyClip::new("c1", "/p.mp4").with_original("/o.mov");
343 clip.status = OfflineStatus::Reconnected;
344 assert!(clip.is_reconnected());
345 }
346
347 #[test]
348 fn test_session_register_and_get() {
349 let mut session = OfflineProxySession::new();
350 session.register(OfflineProxyClip::new("clip001", "/proxy/clip001.mp4"));
351 assert_eq!(session.clip_count(), 1);
352 assert!(session.get("clip001").is_some());
353 assert!(session.get("nonexistent").is_none());
354 }
355
356 #[test]
357 fn test_session_count_by_status() {
358 let mut session = OfflineProxySession::new();
359 session.register(OfflineProxyClip::new("c1", "/p1.mp4"));
360 session.register(OfflineProxyClip::new("c2", "/p2.mp4"));
361 let mut c3 = OfflineProxyClip::new("c3", "/p3.mp4");
362 c3.status = OfflineStatus::Missing;
363 session.register(c3);
364
365 assert_eq!(session.count_by_status(&OfflineStatus::Available), 2);
366 assert_eq!(session.count_by_status(&OfflineStatus::Missing), 1);
367 }
368
369 #[test]
370 fn test_session_reconnect() {
371 let mut session = OfflineProxySession::new();
372 session.register(OfflineProxyClip::new("c1", "/proxy.mp4"));
373 let ok = session.reconnect("c1", "/original.mov");
374 assert!(ok);
375 let clip = session.get("c1").expect("should succeed in test");
376 assert_eq!(clip.status, OfflineStatus::Reconnected);
377 assert!(clip.original_path.is_some());
378 }
379
380 #[test]
381 fn test_session_reconnect_nonexistent() {
382 let mut session = OfflineProxySession::new();
383 let ok = session.reconnect("nonexistent", "/original.mov");
384 assert!(!ok);
385 }
386
387 #[test]
388 fn test_session_substitute() {
389 let mut session = OfflineProxySession::new();
390 session.register(OfflineProxyClip::new("c1", "/proxy.mp4"));
391 session.substitute("c1");
392 assert_eq!(
393 session.get("c1").expect("should succeed in test").status,
394 OfflineStatus::Substituted
395 );
396 }
397
398 #[test]
399 fn test_session_clips_needing_reconnection() {
400 let mut session = OfflineProxySession::new();
401 session.register(OfflineProxyClip::new("c1", "/p1.mp4")); session.register(OfflineProxyClip::new("c2", "/p2.mp4").with_original("/o2.mov"));
403 assert_eq!(session.clips_needing_reconnection().len(), 1);
404 }
405
406 #[test]
407 fn test_reconnect_result_success_rate() {
408 let mut result = ReconnectResult::new();
409 result.reconnected = 8;
410 result.failed = 2;
411 assert!((result.success_rate() - 0.8).abs() < 1e-5);
412 }
413
414 #[test]
415 fn test_reconnect_result_success_rate_empty() {
416 let result = ReconnectResult::new();
417 assert_eq!(result.success_rate(), 1.0);
418 }
419
420 #[test]
421 fn test_auto_reconnector_includes_extension() {
422 let rc = AutoReconnector::new("/media");
423 assert!(rc.includes_extension("mov"));
424 assert!(rc.includes_extension("MXF"));
425 assert!(!rc.includes_extension("avi"));
426 }
427
428 #[test]
429 fn test_auto_reconnector_custom_extensions() {
430 let rc = AutoReconnector::new("/media")
431 .with_extensions(vec!["avi".to_string(), "wmv".to_string()]);
432 assert!(rc.includes_extension("avi"));
433 assert!(!rc.includes_extension("mov"));
434 }
435
436 #[test]
437 fn test_substitution_strategy_default() {
438 let strategy = SubstitutionStrategy::default();
439 assert_eq!(strategy, SubstitutionStrategy::Placeholder);
440 }
441
442 #[test]
443 fn test_session_with_strategy() {
444 let session = OfflineProxySession::new().with_strategy(SubstitutionStrategy::BlackFrame);
445 assert_eq!(
446 session.substitution_strategy(),
447 SubstitutionStrategy::BlackFrame
448 );
449 }
450
451 #[test]
452 fn test_session_strict_mode() {
453 let session = OfflineProxySession::new().strict();
454 assert!(session.is_strict());
455
456 let session2 = OfflineProxySession::new();
457 assert!(!session2.is_strict());
458 }
459}