Skip to main content

oximedia_proxy/
media_link.rs

1//! Media linking between original high-resolution files and their proxies.
2#![allow(dead_code)]
3
4use std::collections::HashMap;
5
6/// Status of a media link between an original and its proxy.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum LinkStatus {
9    /// Both original and proxy exist and are verified.
10    Valid,
11    /// Link record exists but one or both files are missing.
12    Broken,
13    /// Link has not been verified since creation.
14    Unverified,
15    /// Original exists but no proxy has been generated yet.
16    MissingProxy,
17}
18
19impl LinkStatus {
20    /// Return `true` if the link is in a valid, usable state.
21    pub fn is_valid(self) -> bool {
22        matches!(self, Self::Valid)
23    }
24
25    /// Return `true` if the link is broken or unverified.
26    pub fn needs_attention(self) -> bool {
27        matches!(self, Self::Broken | Self::MissingProxy)
28    }
29}
30
31/// A bidirectional link between an original media file and its proxy.
32#[derive(Debug, Clone)]
33pub struct MediaLink {
34    /// Unique link identifier.
35    pub id: u64,
36    /// Path to the original high-resolution file.
37    pub original_path: String,
38    /// Path to the proxy file, if one exists.
39    pub proxy_path: Option<String>,
40    /// Current link status.
41    pub status: LinkStatus,
42    /// Duration in seconds.
43    pub duration_secs: f64,
44}
45
46impl MediaLink {
47    /// Create a new unverified link for an original file.
48    pub fn new(id: u64, original_path: &str, duration_secs: f64) -> Self {
49        Self {
50            id,
51            original_path: original_path.to_owned(),
52            proxy_path: None,
53            status: LinkStatus::MissingProxy,
54            duration_secs,
55        }
56    }
57
58    /// Return `true` if a proxy path is stored in this link.
59    pub fn has_proxy(&self) -> bool {
60        self.proxy_path.is_some()
61    }
62
63    /// Attach a proxy path and mark the link as unverified.
64    pub fn attach_proxy(&mut self, proxy_path: &str) {
65        self.proxy_path = Some(proxy_path.to_owned());
66        self.status = LinkStatus::Unverified;
67    }
68
69    /// Mark the link as verified and valid.
70    pub fn mark_valid(&mut self) {
71        self.status = LinkStatus::Valid;
72    }
73
74    /// Mark the link as broken.
75    pub fn mark_broken(&mut self) {
76        self.status = LinkStatus::Broken;
77    }
78}
79
80/// A store for managing many `MediaLink` records.
81#[derive(Debug, Default)]
82pub struct MediaLinkStore {
83    /// Links indexed by original file path.
84    by_original: HashMap<String, MediaLink>,
85    /// Reverse index: proxy path -> original path.
86    by_proxy: HashMap<String, String>,
87    /// Auto-incrementing link ID counter.
88    next_id: u64,
89}
90
91impl MediaLinkStore {
92    /// Create an empty store.
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Insert or update a link for the given original file.
98    /// Returns the id of the inserted/updated link.
99    pub fn insert(
100        &mut self,
101        original_path: &str,
102        proxy_path: Option<&str>,
103        duration_secs: f64,
104    ) -> u64 {
105        let id = self.next_id;
106        self.next_id += 1;
107        let mut link = MediaLink::new(id, original_path, duration_secs);
108        if let Some(proxy) = proxy_path {
109            link.attach_proxy(proxy);
110            self.by_proxy
111                .insert(proxy.to_owned(), original_path.to_owned());
112        }
113        self.by_original.insert(original_path.to_owned(), link);
114        id
115    }
116
117    /// Look up a link by the original file path.
118    pub fn find_original(&self, original_path: &str) -> Option<&MediaLink> {
119        self.by_original.get(original_path)
120    }
121
122    /// Look up the original link given a proxy path.
123    pub fn find_proxy(&self, proxy_path: &str) -> Option<&MediaLink> {
124        let original = self.by_proxy.get(proxy_path)?;
125        self.by_original.get(original)
126    }
127
128    /// Return a list of original paths that have no proxy attached.
129    pub fn unlinked_originals(&self) -> Vec<&str> {
130        self.by_original
131            .values()
132            .filter(|link| !link.has_proxy())
133            .map(|link| link.original_path.as_str())
134            .collect()
135    }
136
137    /// Return the total number of links in the store.
138    pub fn len(&self) -> usize {
139        self.by_original.len()
140    }
141
142    /// Return `true` if the store is empty.
143    pub fn is_empty(&self) -> bool {
144        self.by_original.is_empty()
145    }
146
147    /// Mark a link as valid by original path. Returns `false` if not found.
148    pub fn verify(&mut self, original_path: &str) -> bool {
149        if let Some(link) = self.by_original.get_mut(original_path) {
150            link.mark_valid();
151            true
152        } else {
153            false
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_link_status_is_valid() {
164        assert!(LinkStatus::Valid.is_valid());
165        assert!(!LinkStatus::Broken.is_valid());
166        assert!(!LinkStatus::Unverified.is_valid());
167        assert!(!LinkStatus::MissingProxy.is_valid());
168    }
169
170    #[test]
171    fn test_link_status_needs_attention() {
172        assert!(LinkStatus::Broken.needs_attention());
173        assert!(LinkStatus::MissingProxy.needs_attention());
174        assert!(!LinkStatus::Valid.needs_attention());
175        assert!(!LinkStatus::Unverified.needs_attention());
176    }
177
178    #[test]
179    fn test_media_link_new_no_proxy() {
180        let link = MediaLink::new(1, "original.mov", 120.0);
181        assert!(!link.has_proxy());
182        assert_eq!(link.status, LinkStatus::MissingProxy);
183    }
184
185    #[test]
186    fn test_media_link_attach_proxy() {
187        let mut link = MediaLink::new(1, "original.mov", 120.0);
188        link.attach_proxy("proxy.mp4");
189        assert!(link.has_proxy());
190        assert_eq!(link.status, LinkStatus::Unverified);
191    }
192
193    #[test]
194    fn test_media_link_mark_valid() {
195        let mut link = MediaLink::new(1, "original.mov", 120.0);
196        link.attach_proxy("proxy.mp4");
197        link.mark_valid();
198        assert!(link.status.is_valid());
199    }
200
201    #[test]
202    fn test_media_link_mark_broken() {
203        let mut link = MediaLink::new(1, "original.mov", 120.0);
204        link.attach_proxy("proxy.mp4");
205        link.mark_broken();
206        assert_eq!(link.status, LinkStatus::Broken);
207    }
208
209    #[test]
210    fn test_store_insert_and_find_original() {
211        let mut store = MediaLinkStore::new();
212        store.insert("orig.mov", Some("proxy.mp4"), 60.0);
213        let link = store
214            .find_original("orig.mov")
215            .expect("should succeed in test");
216        assert_eq!(link.original_path, "orig.mov");
217    }
218
219    #[test]
220    fn test_store_find_proxy() {
221        let mut store = MediaLinkStore::new();
222        store.insert("orig.mov", Some("proxy.mp4"), 60.0);
223        let link = store
224            .find_proxy("proxy.mp4")
225            .expect("should succeed in test");
226        assert_eq!(link.original_path, "orig.mov");
227    }
228
229    #[test]
230    fn test_store_find_proxy_not_found() {
231        let store = MediaLinkStore::new();
232        assert!(store.find_proxy("nonexistent.mp4").is_none());
233    }
234
235    #[test]
236    fn test_store_unlinked_originals() {
237        let mut store = MediaLinkStore::new();
238        store.insert("a.mov", None, 10.0);
239        store.insert("b.mov", Some("b_proxy.mp4"), 10.0);
240        let unlinked = store.unlinked_originals();
241        assert_eq!(unlinked.len(), 1);
242        assert_eq!(unlinked[0], "a.mov");
243    }
244
245    #[test]
246    fn test_store_is_empty() {
247        let store = MediaLinkStore::new();
248        assert!(store.is_empty());
249    }
250
251    #[test]
252    fn test_store_len() {
253        let mut store = MediaLinkStore::new();
254        store.insert("a.mov", None, 10.0);
255        store.insert("b.mov", None, 20.0);
256        assert_eq!(store.len(), 2);
257    }
258
259    #[test]
260    fn test_store_verify_sets_valid() {
261        let mut store = MediaLinkStore::new();
262        store.insert("a.mov", Some("a_proxy.mp4"), 10.0);
263        let result = store.verify("a.mov");
264        assert!(result);
265        let link = store
266            .find_original("a.mov")
267            .expect("should succeed in test");
268        assert!(link.status.is_valid());
269    }
270
271    #[test]
272    fn test_store_verify_missing_returns_false() {
273        let mut store = MediaLinkStore::new();
274        assert!(!store.verify("nonexistent.mov"));
275    }
276}