Skip to main content

oximedia_proxy/
registry.rs

1//! Proxy registry: maps original source files to their proxy counterparts.
2//!
3//! The `ProxyRegistry` is an in-memory (and serializable) mapping from original
4//! media paths to one or more proxy entries, each described by a `ProxySpec`.
5//! It supports multi-resolution proxies per source and can be persisted to JSON.
6
7use crate::spec::{ProxyCodec, ProxySpec};
8use crate::{ProxyError, Result};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13/// Represents a single proxy file registered for an original.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ProxyEntry {
16    /// Path to the proxy file.
17    pub proxy_path: PathBuf,
18    /// The spec used to create this proxy.
19    pub spec: ProxySpec,
20    /// Creation timestamp (Unix seconds, approximate).
21    pub created_at: u64,
22    /// File size in bytes (0 if unknown).
23    pub file_size: u64,
24    /// Whether this proxy has been verified to exist.
25    pub verified: bool,
26}
27
28impl ProxyEntry {
29    /// Create a new proxy entry.
30    #[must_use]
31    pub fn new(proxy_path: PathBuf, spec: ProxySpec) -> Self {
32        Self {
33            proxy_path,
34            spec,
35            created_at: 0,
36            file_size: 0,
37            verified: false,
38        }
39    }
40
41    /// Check if the proxy file exists on disk.
42    #[must_use]
43    pub fn exists(&self) -> bool {
44        self.proxy_path.exists()
45    }
46
47    /// Get the codec used for this proxy.
48    #[must_use]
49    pub fn codec(&self) -> &ProxyCodec {
50        &self.spec.codec
51    }
52}
53
54/// Entry in the registry for a single original file.
55#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56pub struct RegistryRecord {
57    /// Path to the original file.
58    pub original_path: PathBuf,
59    /// Proxies available for this original.
60    pub proxies: Vec<ProxyEntry>,
61    /// User-defined tags.
62    pub tags: Vec<String>,
63}
64
65impl RegistryRecord {
66    /// Create a new registry record for an original path.
67    #[must_use]
68    pub fn new(original_path: PathBuf) -> Self {
69        Self {
70            original_path,
71            proxies: Vec::new(),
72            tags: Vec::new(),
73        }
74    }
75
76    /// Add a proxy entry.
77    pub fn add_proxy(&mut self, entry: ProxyEntry) {
78        self.proxies.push(entry);
79    }
80
81    /// Find a proxy by spec name.
82    #[must_use]
83    pub fn find_proxy_by_spec(&self, spec_name: &str) -> Option<&ProxyEntry> {
84        self.proxies.iter().find(|e| e.spec.name == spec_name)
85    }
86
87    /// Find the best matching proxy for a given maximum video bitrate.
88    #[must_use]
89    pub fn best_proxy_for_bitrate(&self, max_bitrate: u64) -> Option<&ProxyEntry> {
90        self.proxies
91            .iter()
92            .filter(|e| e.spec.video_bitrate <= max_bitrate)
93            .max_by_key(|e| e.spec.video_bitrate)
94    }
95
96    /// Remove all proxies that no longer exist on disk.
97    pub fn purge_missing(&mut self) -> usize {
98        let before = self.proxies.len();
99        self.proxies.retain(|e| e.exists());
100        before - self.proxies.len()
101    }
102}
103
104/// Maps original media paths to their proxy files.
105///
106/// The registry is keyed by the canonical string representation of the original file path.
107#[derive(Debug, Clone, Serialize, Deserialize, Default)]
108pub struct ProxyRegistry {
109    records: HashMap<String, RegistryRecord>,
110    /// Registry version for future compatibility.
111    version: u32,
112}
113
114impl ProxyRegistry {
115    /// Create an empty registry.
116    #[must_use]
117    pub fn new() -> Self {
118        Self {
119            records: HashMap::new(),
120            version: 1,
121        }
122    }
123
124    /// Register a proxy for an original file.
125    ///
126    /// If the original is already registered, the proxy is added to its record.
127    pub fn register(&mut self, original: &Path, proxy_path: &Path, spec: ProxySpec) {
128        let key = original.to_string_lossy().into_owned();
129        let entry = ProxyEntry::new(proxy_path.to_path_buf(), spec);
130        self.records
131            .entry(key)
132            .or_insert_with(|| RegistryRecord::new(original.to_path_buf()))
133            .add_proxy(entry);
134    }
135
136    /// Look up the record for an original file.
137    #[must_use]
138    pub fn get(&self, original: &Path) -> Option<&RegistryRecord> {
139        let key = original.to_string_lossy();
140        self.records.get(key.as_ref())
141    }
142
143    /// Look up the record for an original file (mutable).
144    pub fn get_mut(&mut self, original: &Path) -> Option<&mut RegistryRecord> {
145        let key = original.to_string_lossy().into_owned();
146        self.records.get_mut(&key)
147    }
148
149    /// Remove an original and all its proxies from the registry.
150    ///
151    /// Returns the removed record if it existed.
152    pub fn remove(&mut self, original: &Path) -> Option<RegistryRecord> {
153        let key = original.to_string_lossy().into_owned();
154        self.records.remove(&key)
155    }
156
157    /// Find all proxies with a given spec name across all originals.
158    #[must_use]
159    pub fn find_by_spec(&self, spec_name: &str) -> Vec<(&RegistryRecord, &ProxyEntry)> {
160        self.records
161            .values()
162            .flat_map(|r| {
163                r.proxies
164                    .iter()
165                    .filter(|e| e.spec.name == spec_name)
166                    .map(move |e| (r, e))
167            })
168            .collect()
169    }
170
171    /// Total number of original files registered.
172    #[must_use]
173    pub fn len(&self) -> usize {
174        self.records.len()
175    }
176
177    /// Whether the registry is empty.
178    #[must_use]
179    pub fn is_empty(&self) -> bool {
180        self.records.is_empty()
181    }
182
183    /// Total number of proxy entries across all originals.
184    #[must_use]
185    pub fn proxy_count(&self) -> usize {
186        self.records.values().map(|r| r.proxies.len()).sum()
187    }
188
189    /// Purge all proxy entries that don't exist on disk.
190    ///
191    /// Returns total number of entries removed.
192    pub fn purge_missing(&mut self) -> usize {
193        self.records
194            .values_mut()
195            .map(RegistryRecord::purge_missing)
196            .sum()
197    }
198
199    /// Remove originals that have no proxies.
200    ///
201    /// Returns number of originals removed.
202    pub fn remove_empty_records(&mut self) -> usize {
203        let before = self.records.len();
204        self.records.retain(|_, r| !r.proxies.is_empty());
205        before - self.records.len()
206    }
207
208    /// Serialize the registry to JSON.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if serialization fails.
213    pub fn to_json(&self) -> Result<String> {
214        serde_json::to_string_pretty(self).map_err(|e| ProxyError::MetadataError(e.to_string()))
215    }
216
217    /// Deserialize the registry from JSON.
218    ///
219    /// # Errors
220    ///
221    /// Returns an error if deserialization fails.
222    pub fn from_json(json: &str) -> Result<Self> {
223        serde_json::from_str(json).map_err(|e| ProxyError::MetadataError(e.to_string()))
224    }
225
226    /// Save registry to a JSON file.
227    ///
228    /// # Errors
229    ///
230    /// Returns an error if the file cannot be written.
231    pub fn save(&self, path: &Path) -> Result<()> {
232        let json = self.to_json()?;
233        std::fs::write(path, json).map_err(ProxyError::IoError)
234    }
235
236    /// Load registry from a JSON file.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if the file cannot be read or parsed.
241    pub fn load(path: &Path) -> Result<Self> {
242        let content = std::fs::read_to_string(path).map_err(ProxyError::IoError)?;
243        Self::from_json(&content)
244    }
245
246    /// Iterate over all records.
247    pub fn iter(&self) -> impl Iterator<Item = (&str, &RegistryRecord)> {
248        self.records.iter().map(|(k, v)| (k.as_str(), v))
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::spec::{ProxyResolutionMode, ProxySpec};
256
257    fn make_spec(name: &str) -> ProxySpec {
258        ProxySpec::new(
259            name,
260            ProxyResolutionMode::ScaleFactor(0.25),
261            ProxyCodec::H264,
262            2_000_000,
263        )
264    }
265
266    #[test]
267    fn test_proxy_entry_new() {
268        let entry = ProxyEntry::new(PathBuf::from("/tmp/proxy.mp4"), make_spec("Test"));
269        assert_eq!(entry.proxy_path, PathBuf::from("/tmp/proxy.mp4"));
270        assert_eq!(entry.spec.name, "Test");
271        assert!(!entry.verified);
272    }
273
274    #[test]
275    fn test_proxy_entry_codec() {
276        let entry = ProxyEntry::new(PathBuf::from("/tmp/p.mp4"), make_spec("Q"));
277        assert_eq!(entry.codec(), &ProxyCodec::H264);
278    }
279
280    #[test]
281    fn test_proxy_entry_not_exists() {
282        let entry = ProxyEntry::new(PathBuf::from("/nonexistent/proxy.mp4"), make_spec("Q"));
283        assert!(!entry.exists());
284    }
285
286    #[test]
287    fn test_registry_record_new() {
288        let rec = RegistryRecord::new(PathBuf::from("/src/clip.mov"));
289        assert_eq!(rec.original_path, PathBuf::from("/src/clip.mov"));
290        assert!(rec.proxies.is_empty());
291    }
292
293    #[test]
294    fn test_registry_record_add_proxy() {
295        let mut rec = RegistryRecord::new(PathBuf::from("/src/clip.mov"));
296        rec.add_proxy(ProxyEntry::new(
297            PathBuf::from("/proxy/clip.mp4"),
298            make_spec("Q"),
299        ));
300        assert_eq!(rec.proxies.len(), 1);
301    }
302
303    #[test]
304    fn test_registry_record_find_by_spec() {
305        let mut rec = RegistryRecord::new(PathBuf::from("/src/clip.mov"));
306        rec.add_proxy(ProxyEntry::new(
307            PathBuf::from("/proxy/clip.mp4"),
308            make_spec("Quarter"),
309        ));
310        rec.add_proxy(ProxyEntry::new(
311            PathBuf::from("/proxy/clip_h.mp4"),
312            make_spec("Half"),
313        ));
314        assert!(rec.find_proxy_by_spec("Quarter").is_some());
315        assert!(rec.find_proxy_by_spec("Half").is_some());
316        assert!(rec.find_proxy_by_spec("Missing").is_none());
317    }
318
319    #[test]
320    fn test_registry_record_best_proxy_for_bitrate() {
321        let mut rec = RegistryRecord::new(PathBuf::from("/src/clip.mov"));
322        rec.add_proxy(ProxyEntry::new(PathBuf::from("/p1.mp4"), make_spec("Low")));
323        let mut high_spec = make_spec("High");
324        high_spec.video_bitrate = 10_000_000;
325        rec.add_proxy(ProxyEntry::new(PathBuf::from("/p2.mp4"), high_spec));
326
327        let best = rec
328            .best_proxy_for_bitrate(3_000_000)
329            .expect("should succeed in test");
330        assert_eq!(best.spec.name, "Low");
331
332        let best_all = rec
333            .best_proxy_for_bitrate(100_000_000)
334            .expect("should succeed in test");
335        assert_eq!(best_all.spec.name, "High");
336    }
337
338    #[test]
339    fn test_proxy_registry_new() {
340        let reg = ProxyRegistry::new();
341        assert!(reg.is_empty());
342        assert_eq!(reg.len(), 0);
343        assert_eq!(reg.proxy_count(), 0);
344    }
345
346    #[test]
347    fn test_proxy_registry_register_and_get() {
348        let mut reg = ProxyRegistry::new();
349        let original = Path::new("/media/clip001.mov");
350        let proxy = Path::new("/proxy/clip001.mp4");
351        reg.register(original, proxy, make_spec("Quarter"));
352        assert_eq!(reg.len(), 1);
353        assert_eq!(reg.proxy_count(), 1);
354
355        let rec = reg.get(original).expect("should succeed in test");
356        assert_eq!(rec.proxies.len(), 1);
357        assert_eq!(
358            rec.proxies[0].proxy_path,
359            PathBuf::from("/proxy/clip001.mp4")
360        );
361    }
362
363    #[test]
364    fn test_proxy_registry_multiple_proxies_per_original() {
365        let mut reg = ProxyRegistry::new();
366        let original = Path::new("/media/clip001.mov");
367        reg.register(original, Path::new("/proxy/q.mp4"), make_spec("Quarter"));
368        reg.register(original, Path::new("/proxy/h.mp4"), make_spec("Half"));
369        assert_eq!(reg.len(), 1);
370        assert_eq!(reg.proxy_count(), 2);
371    }
372
373    #[test]
374    fn test_proxy_registry_remove() {
375        let mut reg = ProxyRegistry::new();
376        let original = Path::new("/media/clip001.mov");
377        reg.register(original, Path::new("/p.mp4"), make_spec("Q"));
378        let removed = reg.remove(original);
379        assert!(removed.is_some());
380        assert!(reg.is_empty());
381    }
382
383    #[test]
384    fn test_proxy_registry_find_by_spec() {
385        let mut reg = ProxyRegistry::new();
386        reg.register(
387            Path::new("/a.mov"),
388            Path::new("/pa.mp4"),
389            make_spec("Quarter"),
390        );
391        reg.register(
392            Path::new("/b.mov"),
393            Path::new("/pb.mp4"),
394            make_spec("Quarter"),
395        );
396        reg.register(Path::new("/c.mov"), Path::new("/pc.mp4"), make_spec("Half"));
397        let matches = reg.find_by_spec("Quarter");
398        assert_eq!(matches.len(), 2);
399    }
400
401    #[test]
402    fn test_proxy_registry_json_roundtrip() {
403        let mut reg = ProxyRegistry::new();
404        reg.register(
405            Path::new("/orig.mov"),
406            Path::new("/proxy.mp4"),
407            make_spec("Q"),
408        );
409        let json = reg.to_json().expect("should succeed in test");
410        assert!(!json.is_empty());
411        let loaded = ProxyRegistry::from_json(&json).expect("should succeed in test");
412        assert_eq!(loaded.len(), 1);
413        assert_eq!(loaded.proxy_count(), 1);
414    }
415
416    #[test]
417    fn test_proxy_registry_remove_empty_records() {
418        let mut reg = ProxyRegistry::new();
419        // Add a record with no proxies (manually)
420        reg.records.insert(
421            "/empty.mov".to_string(),
422            RegistryRecord::new(PathBuf::from("/empty.mov")),
423        );
424        reg.register(
425            Path::new("/full.mov"),
426            Path::new("/proxy.mp4"),
427            make_spec("Q"),
428        );
429        assert_eq!(reg.len(), 2);
430        let removed = reg.remove_empty_records();
431        assert_eq!(removed, 1);
432        assert_eq!(reg.len(), 1);
433    }
434
435    #[test]
436    fn test_proxy_registry_iter() {
437        let mut reg = ProxyRegistry::new();
438        reg.register(Path::new("/a.mov"), Path::new("/p.mp4"), make_spec("Q"));
439        let count = reg.iter().count();
440        assert_eq!(count, 1);
441    }
442}