Skip to main content

oximedia_proxy/
proxy_index.rs

1//! In-memory index mapping original media paths to proxy entries.
2//!
3//! Provides `ProxyEntry` and `ProxyIndex` for fast lookup of proxies
4//! by their originating source file path.
5
6#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10/// A record in the proxy index describing one proxy asset.
11#[derive(Debug, Clone, PartialEq)]
12pub struct ProxyEntry {
13    /// Absolute path to the original high-resolution media file.
14    pub original_path: String,
15    /// Absolute path to the proxy media file.
16    pub proxy_path: String,
17    /// Width of the proxy in pixels.
18    pub width: u32,
19    /// Height of the proxy in pixels.
20    pub height: u32,
21    /// Bitrate of the proxy in kbps.
22    pub bitrate_kbps: u32,
23    /// Optional codec identifier (e.g. "h264").
24    pub codec: Option<String>,
25}
26
27impl ProxyEntry {
28    /// Create a new proxy entry.
29    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    /// Return `true` when required fields are non-empty and dimensions are > 0.
47    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    /// Return a display label combining resolution and bitrate.
56    pub fn display_label(&self) -> String {
57        format!("{}x{}@{}kbps", self.width, self.height, self.bitrate_kbps)
58    }
59
60    /// Return total pixel count (width × height).
61    pub fn pixel_count(&self) -> u64 {
62        u64::from(self.width) * u64::from(self.height)
63    }
64}
65
66/// An in-memory index of proxy entries keyed by original file path.
67#[derive(Debug, Default)]
68pub struct ProxyIndex {
69    // Maps original_path → Vec<ProxyEntry> (multiple qualities possible).
70    map: HashMap<String, Vec<ProxyEntry>>,
71}
72
73impl ProxyIndex {
74    /// Create an empty index.
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Insert a `ProxyEntry`.  Entries with the same `original_path` are accumulated.
80    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    /// Find all proxy entries for a given original path.
88    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    /// Remove all entries for a given original path.  Returns the removed entries.
96    pub fn remove(&mut self, original_path: &str) -> Vec<ProxyEntry> {
97        self.map.remove(original_path).unwrap_or_default()
98    }
99
100    /// Return the total number of proxy entries across all originals.
101    pub fn count(&self) -> usize {
102        self.map.values().map(Vec::len).sum()
103    }
104
105    /// Return the number of unique originals in the index.
106    pub fn original_count(&self) -> usize {
107        self.map.len()
108    }
109
110    /// Return `true` if the index is empty.
111    pub fn is_empty(&self) -> bool {
112        self.map.is_empty()
113    }
114
115    /// Return all entries as a flat iterator.
116    pub fn all_entries(&self) -> impl Iterator<Item = &ProxyEntry> {
117        self.map.values().flat_map(|v| v.iter())
118    }
119
120    /// Return `true` if any proxy entry exists for the given original path.
121    pub fn contains(&self, original_path: &str) -> bool {
122        self.map.contains_key(original_path)
123    }
124
125    /// Find the entry with the highest bitrate for a given original path.
126    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}