Skip to main content

oximedia_proxy/conform/
mapper.rs

1//! Media path mapping utilities for conforming.
2
3use crate::Result;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6
7/// Media path mapper for relinking proxy paths to original paths.
8pub struct PathMapper {
9    /// Mapping from proxy paths to original paths.
10    mappings: HashMap<PathBuf, PathBuf>,
11
12    /// Base proxy directory.
13    proxy_base: Option<PathBuf>,
14
15    /// Base original directory.
16    original_base: Option<PathBuf>,
17
18    /// Use case-insensitive matching.
19    case_insensitive: bool,
20}
21
22impl PathMapper {
23    /// Create a new path mapper.
24    #[must_use]
25    pub fn new() -> Self {
26        Self {
27            mappings: HashMap::new(),
28            proxy_base: None,
29            original_base: None,
30            case_insensitive: false,
31        }
32    }
33
34    /// Set the proxy base directory.
35    #[must_use]
36    pub fn with_proxy_base(mut self, base: PathBuf) -> Self {
37        self.proxy_base = Some(base);
38        self
39    }
40
41    /// Set the original base directory.
42    #[must_use]
43    pub fn with_original_base(mut self, base: PathBuf) -> Self {
44        self.original_base = Some(base);
45        self
46    }
47
48    /// Enable case-insensitive matching.
49    #[must_use]
50    pub const fn case_insensitive(mut self, enabled: bool) -> Self {
51        self.case_insensitive = enabled;
52        self
53    }
54
55    /// Add a path mapping.
56    pub fn add_mapping(&mut self, proxy: PathBuf, original: PathBuf) {
57        self.mappings.insert(proxy, original);
58    }
59
60    /// Map a proxy path to an original path.
61    #[must_use]
62    pub fn map(&self, proxy_path: &Path) -> Option<PathBuf> {
63        // Try direct mapping first
64        if let Some(original) = self.mappings.get(proxy_path) {
65            return Some(original.clone());
66        }
67
68        // Try base directory mapping
69        if let (Some(proxy_base), Some(original_base)) = (&self.proxy_base, &self.original_base) {
70            if let Ok(relative) = proxy_path.strip_prefix(proxy_base) {
71                return Some(original_base.join(relative));
72            }
73        }
74
75        // Try case-insensitive matching
76        if self.case_insensitive {
77            let proxy_lower = proxy_path.to_string_lossy().to_lowercase();
78            for (key, value) in &self.mappings {
79                if key.to_string_lossy().to_lowercase() == proxy_lower {
80                    return Some(value.clone());
81                }
82            }
83        }
84
85        None
86    }
87
88    /// Map multiple paths.
89    #[must_use]
90    pub fn map_batch(&self, proxy_paths: &[PathBuf]) -> Vec<MappingResult> {
91        proxy_paths
92            .iter()
93            .map(|proxy| {
94                if let Some(original) = self.map(proxy) {
95                    MappingResult::Success {
96                        proxy: proxy.clone(),
97                        original,
98                    }
99                } else {
100                    MappingResult::Failed {
101                        proxy: proxy.clone(),
102                    }
103                }
104            })
105            .collect()
106    }
107
108    /// Clear all mappings.
109    pub fn clear(&mut self) {
110        self.mappings.clear();
111    }
112
113    /// Get the number of mappings.
114    #[must_use]
115    pub fn count(&self) -> usize {
116        self.mappings.len()
117    }
118
119    /// Get all proxy paths.
120    #[must_use]
121    pub fn proxy_paths(&self) -> Vec<PathBuf> {
122        self.mappings.keys().cloned().collect()
123    }
124
125    /// Get all original paths.
126    #[must_use]
127    pub fn original_paths(&self) -> Vec<PathBuf> {
128        self.mappings.values().cloned().collect()
129    }
130}
131
132impl Default for PathMapper {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138/// Path mapping result.
139#[derive(Debug, Clone)]
140pub enum MappingResult {
141    /// Successful mapping.
142    Success {
143        /// Proxy path.
144        proxy: PathBuf,
145        /// Original path.
146        original: PathBuf,
147    },
148    /// Failed mapping.
149    Failed {
150        /// Proxy path.
151        proxy: PathBuf,
152    },
153}
154
155impl MappingResult {
156    /// Check if mapping was successful.
157    #[must_use]
158    pub const fn is_success(&self) -> bool {
159        matches!(self, Self::Success { .. })
160    }
161
162    /// Get the proxy path.
163    #[must_use]
164    pub fn proxy(&self) -> &Path {
165        match self {
166            Self::Success { proxy, .. } | Self::Failed { proxy } => proxy,
167        }
168    }
169}
170
171/// Automatic path mapper that tries to infer mappings.
172pub struct AutoPathMapper {
173    mapper: PathMapper,
174}
175
176impl AutoPathMapper {
177    /// Create a new automatic path mapper.
178    #[must_use]
179    pub fn new() -> Self {
180        Self {
181            mapper: PathMapper::new(),
182        }
183    }
184
185    /// Auto-detect mappings based on filename matching.
186    pub fn auto_detect(&mut self, proxy_dir: &Path, original_dir: &Path) -> Result<usize> {
187        let mut count = 0;
188
189        // Scan proxy directory
190        if let Ok(entries) = std::fs::read_dir(proxy_dir) {
191            for entry in entries.flatten() {
192                if let Ok(metadata) = entry.metadata() {
193                    if metadata.is_file() {
194                        let proxy_path = entry.path();
195
196                        // Try to find matching original
197                        if let Some(original_path) =
198                            self.find_matching_original(&proxy_path, original_dir)
199                        {
200                            self.mapper.add_mapping(proxy_path, original_path);
201                            count += 1;
202                        }
203                    }
204                }
205            }
206        }
207
208        Ok(count)
209    }
210
211    /// Find matching original file for a proxy.
212    fn find_matching_original(&self, proxy: &Path, original_dir: &Path) -> Option<PathBuf> {
213        // Get proxy filename without extension
214        let proxy_stem = proxy.file_stem()?.to_str()?;
215
216        // Scan original directory for matches
217        if let Ok(entries) = std::fs::read_dir(original_dir) {
218            for entry in entries.flatten() {
219                if let Some(filename) = entry.file_name().to_str() {
220                    // Simple filename matching
221                    if filename.contains(proxy_stem) {
222                        return Some(entry.path());
223                    }
224                }
225            }
226        }
227
228        None
229    }
230
231    /// Get the underlying path mapper.
232    #[must_use]
233    pub const fn mapper(&self) -> &PathMapper {
234        &self.mapper
235    }
236}
237
238impl Default for AutoPathMapper {
239    fn default() -> Self {
240        Self::new()
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_path_mapper() {
250        let mut mapper = PathMapper::new();
251
252        mapper.add_mapping(PathBuf::from("proxy.mp4"), PathBuf::from("original.mov"));
253
254        let result = mapper.map(Path::new("proxy.mp4"));
255        assert!(result.is_some());
256        assert_eq!(
257            result.expect("should succeed in test"),
258            PathBuf::from("original.mov")
259        );
260
261        assert_eq!(mapper.count(), 1);
262    }
263
264    #[test]
265    fn test_path_mapper_with_bases() {
266        let mapper = PathMapper::new()
267            .with_proxy_base(PathBuf::from("/proxies"))
268            .with_original_base(PathBuf::from("/originals"));
269
270        let result = mapper.map(Path::new("/proxies/clip1.mp4"));
271        assert!(result.is_some());
272        assert_eq!(
273            result.expect("should succeed in test"),
274            PathBuf::from("/originals/clip1.mp4")
275        );
276    }
277
278    #[test]
279    fn test_mapping_result() {
280        let result = MappingResult::Success {
281            proxy: PathBuf::from("proxy.mp4"),
282            original: PathBuf::from("original.mov"),
283        };
284
285        assert!(result.is_success());
286        assert_eq!(result.proxy(), Path::new("proxy.mp4"));
287
288        let result = MappingResult::Failed {
289            proxy: PathBuf::from("proxy.mp4"),
290        };
291
292        assert!(!result.is_success());
293    }
294
295    #[test]
296    fn test_batch_mapping() {
297        let mut mapper = PathMapper::new();
298        mapper.add_mapping(PathBuf::from("proxy1.mp4"), PathBuf::from("original1.mov"));
299        mapper.add_mapping(PathBuf::from("proxy2.mp4"), PathBuf::from("original2.mov"));
300
301        let proxies = vec![
302            PathBuf::from("proxy1.mp4"),
303            PathBuf::from("proxy2.mp4"),
304            PathBuf::from("proxy3.mp4"),
305        ];
306
307        let results = mapper.map_batch(&proxies);
308        assert_eq!(results.len(), 3);
309        assert!(results[0].is_success());
310        assert!(results[1].is_success());
311        assert!(!results[2].is_success());
312    }
313
314    #[test]
315    fn test_auto_path_mapper() {
316        let auto_mapper = AutoPathMapper::new();
317        assert_eq!(auto_mapper.mapper().count(), 0);
318    }
319}