1use crate::spec::{ProxyCodec, ProxySpec};
8use crate::{ProxyError, Result};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ProxyEntry {
16 pub proxy_path: PathBuf,
18 pub spec: ProxySpec,
20 pub created_at: u64,
22 pub file_size: u64,
24 pub verified: bool,
26}
27
28impl ProxyEntry {
29 #[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 #[must_use]
43 pub fn exists(&self) -> bool {
44 self.proxy_path.exists()
45 }
46
47 #[must_use]
49 pub fn codec(&self) -> &ProxyCodec {
50 &self.spec.codec
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56pub struct RegistryRecord {
57 pub original_path: PathBuf,
59 pub proxies: Vec<ProxyEntry>,
61 pub tags: Vec<String>,
63}
64
65impl RegistryRecord {
66 #[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 pub fn add_proxy(&mut self, entry: ProxyEntry) {
78 self.proxies.push(entry);
79 }
80
81 #[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 #[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 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
108pub struct ProxyRegistry {
109 records: HashMap<String, RegistryRecord>,
110 version: u32,
112}
113
114impl ProxyRegistry {
115 #[must_use]
117 pub fn new() -> Self {
118 Self {
119 records: HashMap::new(),
120 version: 1,
121 }
122 }
123
124 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 #[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 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 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 #[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 #[must_use]
173 pub fn len(&self) -> usize {
174 self.records.len()
175 }
176
177 #[must_use]
179 pub fn is_empty(&self) -> bool {
180 self.records.is_empty()
181 }
182
183 #[must_use]
185 pub fn proxy_count(&self) -> usize {
186 self.records.values().map(|r| r.proxies.len()).sum()
187 }
188
189 pub fn purge_missing(&mut self) -> usize {
193 self.records
194 .values_mut()
195 .map(RegistryRecord::purge_missing)
196 .sum()
197 }
198
199 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 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 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 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 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 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 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}