Skip to main content

oximedia_clips/
manager.rs

1//! Main clip manager that integrates all functionality.
2
3use crate::clip::{Clip, ClipId};
4use crate::database::ClipDatabase;
5use crate::error::{ClipError, ClipResult};
6use crate::export::{ClipListExporter, EdlExporter};
7use crate::group::{Bin, BinId, Collection, CollectionId, Folder, FolderId, SmartCollection};
8use crate::import::{BatchImporter, MediaScanner};
9use crate::marker::{Marker, MarkerId, MarkerManager};
10use crate::proxy::{ProxyLink, ProxyManager};
11use crate::search::{ClipFilter, SearchEngine};
12use crate::take::{Take, TakeManager};
13use oximedia_core::types::Rational;
14use std::collections::HashMap;
15use std::path::{Path, PathBuf};
16
17/// Main clip management system.
18pub struct ClipManager {
19    database: ClipDatabase,
20    marker_manager: MarkerManager,
21    take_manager: TakeManager,
22    proxy_manager: ProxyManager,
23    #[allow(dead_code)]
24    search_engine: SearchEngine,
25    bins: HashMap<BinId, Bin>,
26    folders: HashMap<FolderId, Folder>,
27    collections: HashMap<CollectionId, Collection>,
28    smart_collections: Vec<SmartCollection>,
29}
30
31impl ClipManager {
32    /// Creates a new clip manager.
33    ///
34    /// # Errors
35    ///
36    /// Returns an error if the database cannot be initialized.
37    pub async fn new(database_url: impl AsRef<str>) -> ClipResult<Self> {
38        let database = ClipDatabase::new(database_url).await?;
39
40        Ok(Self {
41            database,
42            marker_manager: MarkerManager::new(),
43            take_manager: TakeManager::new(),
44            proxy_manager: ProxyManager::new(),
45            search_engine: SearchEngine::new(),
46            bins: HashMap::new(),
47            folders: HashMap::new(),
48            collections: HashMap::new(),
49            smart_collections: Vec::new(),
50        })
51    }
52
53    // Clip operations
54
55    /// Adds a clip to the database.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the clip cannot be saved.
60    pub async fn add_clip(&self, clip: Clip) -> ClipResult<ClipId> {
61        let clip_id = clip.id;
62        self.database.save_clip(&clip).await?;
63        Ok(clip_id)
64    }
65
66    /// Gets a clip by ID.
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if the clip is not found.
71    pub async fn get_clip(&self, clip_id: &ClipId) -> ClipResult<Clip> {
72        self.database.get_clip(clip_id).await
73    }
74
75    /// Updates a clip.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if the clip cannot be updated.
80    pub async fn update_clip(&self, clip: Clip) -> ClipResult<()> {
81        self.database.save_clip(&clip).await
82    }
83
84    /// Deletes a clip.
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if the clip cannot be deleted.
89    pub async fn delete_clip(&self, clip_id: &ClipId) -> ClipResult<()> {
90        self.database.delete_clip(clip_id).await
91    }
92
93    /// Gets all clips.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if clips cannot be loaded.
98    pub async fn get_all_clips(&self) -> ClipResult<Vec<Clip>> {
99        self.database.get_all_clips().await
100    }
101
102    /// Returns the number of clips.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the count fails.
107    pub async fn clip_count(&self) -> ClipResult<i64> {
108        self.database.count_clips().await
109    }
110
111    /// Adds multiple clips in a single database transaction (bulk import).
112    ///
113    /// Significantly more efficient than calling `add_clip()` in a loop.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if the transaction or any individual save fails.
118    pub async fn add_clips(&self, clips: Vec<Clip>) -> ClipResult<Vec<ClipId>> {
119        let ids: Vec<ClipId> = clips.iter().map(|c| c.id).collect();
120        self.database.batch_save_clips(&clips).await?;
121        Ok(ids)
122    }
123
124    /// Lists clips with pagination.
125    ///
126    /// `page` is 0-indexed; `page_size` is the number of clips per page.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if the query fails.
131    pub async fn list_clips(&self, page: i64, page_size: i64) -> ClipResult<Vec<Clip>> {
132        self.database.get_clips_page(page, page_size).await
133    }
134
135    /// Searches clips by query string with pagination.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if the search fails.
140    pub async fn search_paged(
141        &self,
142        query: &str,
143        page: i64,
144        page_size: i64,
145    ) -> ClipResult<Vec<Clip>> {
146        self.database
147            .search_clips_page(query, page, page_size)
148            .await
149    }
150
151    // Search operations
152
153    /// Searches clips by query string.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if the search fails.
158    pub async fn search(&self, query: &str) -> ClipResult<Vec<Clip>> {
159        self.database.search_clips(query).await
160    }
161
162    /// Filters clips using advanced criteria.
163    ///
164    /// # Errors
165    ///
166    /// Returns an error if the filter operation fails.
167    pub async fn filter(&self, filter: &ClipFilter) -> ClipResult<Vec<Clip>> {
168        let clips = self.database.get_all_clips().await?;
169        Ok(filter.apply(&clips).into_iter().cloned().collect())
170    }
171
172    // Bin operations
173
174    /// Creates a new bin.
175    pub fn create_bin(&mut self, name: impl Into<String>) -> BinId {
176        let bin = Bin::new(name);
177        let bin_id = bin.id;
178        self.bins.insert(bin_id, bin);
179        bin_id
180    }
181
182    /// Gets a bin by ID.
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if the bin is not found.
187    pub fn get_bin(&self, bin_id: &BinId) -> ClipResult<&Bin> {
188        self.bins
189            .get(bin_id)
190            .ok_or_else(|| ClipError::BinNotFound(bin_id.to_string()))
191    }
192
193    /// Gets a mutable bin by ID.
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if the bin is not found.
198    pub fn get_bin_mut(&mut self, bin_id: &BinId) -> ClipResult<&mut Bin> {
199        self.bins
200            .get_mut(bin_id)
201            .ok_or_else(|| ClipError::BinNotFound(bin_id.to_string()))
202    }
203
204    /// Adds a clip to a bin.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if the bin is not found.
209    pub fn add_clip_to_bin(&mut self, bin_id: &BinId, clip_id: ClipId) -> ClipResult<()> {
210        let bin = self.get_bin_mut(bin_id)?;
211        bin.add_clip(clip_id);
212        Ok(())
213    }
214
215    /// Lists all bins.
216    #[must_use]
217    pub fn list_bins(&self) -> Vec<&Bin> {
218        self.bins.values().collect()
219    }
220
221    // Folder operations
222
223    /// Creates a new folder.
224    pub fn create_folder(&mut self, name: impl Into<String>) -> FolderId {
225        let folder = Folder::new(name);
226        let folder_id = folder.id;
227        self.folders.insert(folder_id, folder);
228        folder_id
229    }
230
231    /// Creates a child folder.
232    pub fn create_child_folder(
233        &mut self,
234        name: impl Into<String>,
235        parent_id: FolderId,
236    ) -> FolderId {
237        let folder = Folder::new_child(name, parent_id);
238        let folder_id = folder.id;
239        self.folders.insert(folder_id, folder);
240        folder_id
241    }
242
243    /// Gets a folder by ID.
244    ///
245    /// # Errors
246    ///
247    /// Returns an error if the folder is not found.
248    pub fn get_folder(&self, folder_id: &FolderId) -> ClipResult<&Folder> {
249        self.folders
250            .get(folder_id)
251            .ok_or_else(|| ClipError::FolderNotFound(folder_id.to_string()))
252    }
253
254    // Collection operations
255
256    /// Creates a new collection.
257    pub fn create_collection(&mut self, name: impl Into<String>) -> CollectionId {
258        let collection = Collection::new(name);
259        let collection_id = collection.id;
260        self.collections.insert(collection_id, collection);
261        collection_id
262    }
263
264    /// Gets a collection by ID.
265    ///
266    /// # Errors
267    ///
268    /// Returns an error if the collection is not found.
269    pub fn get_collection(&self, collection_id: &CollectionId) -> ClipResult<&Collection> {
270        self.collections
271            .get(collection_id)
272            .ok_or_else(|| ClipError::CollectionNotFound(collection_id.to_string()))
273    }
274
275    /// Adds a clip to a collection.
276    ///
277    /// # Errors
278    ///
279    /// Returns an error if the collection is not found.
280    pub fn add_clip_to_collection(
281        &mut self,
282        collection_id: &CollectionId,
283        clip_id: ClipId,
284    ) -> ClipResult<()> {
285        let collection = self
286            .collections
287            .get_mut(collection_id)
288            .ok_or_else(|| ClipError::CollectionNotFound(collection_id.to_string()))?;
289        collection.add_clip(clip_id);
290        Ok(())
291    }
292
293    // Smart collection operations
294
295    /// Creates a new smart collection.
296    pub fn create_smart_collection(&mut self, smart_collection: SmartCollection) {
297        self.smart_collections.push(smart_collection);
298    }
299
300    /// Updates all smart collections.
301    ///
302    /// # Errors
303    ///
304    /// Returns an error if clips cannot be loaded.
305    pub async fn update_smart_collections(&mut self) -> ClipResult<()> {
306        let clips = self.database.get_all_clips().await?;
307
308        for smart_collection in &mut self.smart_collections {
309            smart_collection.update(&clips);
310        }
311
312        Ok(())
313    }
314
315    // Marker operations
316
317    /// Adds a marker to a clip.
318    pub fn add_marker(&mut self, clip_id: ClipId, marker: Marker) {
319        self.marker_manager.add_marker(clip_id, marker);
320    }
321
322    /// Gets markers for a clip.
323    #[must_use]
324    pub fn get_markers(&self, clip_id: &ClipId) -> Vec<&Marker> {
325        self.marker_manager.get_markers(clip_id)
326    }
327
328    /// Removes a marker.
329    ///
330    /// # Errors
331    ///
332    /// Returns an error if the marker is not found.
333    pub fn remove_marker(&mut self, clip_id: &ClipId, marker_id: &MarkerId) -> ClipResult<()> {
334        self.marker_manager.remove_marker(clip_id, marker_id)
335    }
336
337    // Take operations
338
339    /// Adds a take.
340    pub fn add_take(&mut self, take: Take) {
341        self.take_manager.add_take(take);
342    }
343
344    /// Gets takes for a scene.
345    #[must_use]
346    pub fn get_scene_takes(&self, scene: &str) -> Vec<&Take> {
347        self.take_manager.get_scene_takes(scene)
348    }
349
350    /// Gets takes for a clip.
351    #[must_use]
352    pub fn get_clip_takes(&self, clip_id: &ClipId) -> Vec<&Take> {
353        self.take_manager.get_clip_takes(clip_id)
354    }
355
356    // Proxy operations
357
358    /// Adds a proxy link.
359    pub fn add_proxy(&mut self, link: ProxyLink) {
360        self.proxy_manager.add_link(link);
361    }
362
363    /// Gets proxy links for a clip.
364    #[must_use]
365    pub fn get_proxies(&self, clip_id: &ClipId) -> Vec<&ProxyLink> {
366        self.proxy_manager.get_links(clip_id)
367    }
368
369    /// Gets the best proxy for a clip.
370    #[must_use]
371    pub fn get_best_proxy(&self, clip_id: &ClipId) -> Option<&ProxyLink> {
372        self.proxy_manager.get_best_proxy(clip_id)
373    }
374
375    // Import operations
376
377    /// Scans a directory for media files.
378    ///
379    /// # Errors
380    ///
381    /// Returns an error if the scan fails.
382    pub async fn scan_directory(&self, path: impl AsRef<Path>) -> ClipResult<Vec<Clip>> {
383        let scanner = MediaScanner::new();
384        scanner.scan(path).await
385    }
386
387    /// Imports clips from file paths.
388    #[must_use]
389    pub fn import_clips(&self, paths: Vec<PathBuf>) -> Vec<Clip> {
390        let importer = BatchImporter::default();
391        importer.import(paths)
392    }
393
394    // Export operations
395
396    /// Exports clips to CSV.
397    ///
398    /// # Errors
399    ///
400    /// Returns an error if the export fails.
401    pub async fn export_csv(&self, clip_ids: &[ClipId]) -> ClipResult<String> {
402        let mut clips = Vec::new();
403        for clip_id in clip_ids {
404            clips.push(self.database.get_clip(clip_id).await?);
405        }
406
407        let exporter = ClipListExporter::new();
408        exporter.to_csv(&clips)
409    }
410
411    /// Exports clips to EDL.
412    ///
413    /// # Errors
414    ///
415    /// Returns an error if the export fails.
416    pub async fn export_edl(
417        &self,
418        clip_ids: &[ClipId],
419        frame_rate: Rational,
420    ) -> ClipResult<String> {
421        let mut clips = Vec::new();
422        for clip_id in clip_ids {
423            clips.push(self.database.get_clip(clip_id).await?);
424        }
425
426        let exporter = EdlExporter::new(frame_rate);
427        exporter.to_edl(&clips)
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[tokio::test]
436    async fn test_clip_manager() {
437        let manager = ClipManager::new(":memory:")
438            .await
439            .expect("new should succeed");
440        let count = manager
441            .clip_count()
442            .await
443            .expect("clip_count should succeed");
444        assert_eq!(count, 0);
445    }
446
447    #[tokio::test]
448    async fn test_add_and_get_clip() {
449        let manager = ClipManager::new(":memory:")
450            .await
451            .expect("new should succeed");
452
453        let clip = Clip::new(PathBuf::from("/test.mov"));
454        let clip_id = manager
455            .add_clip(clip.clone())
456            .await
457            .expect("add_clip should succeed");
458
459        let loaded = manager
460            .get_clip(&clip_id)
461            .await
462            .expect("get_clip should succeed");
463        assert_eq!(loaded.id, clip_id);
464    }
465
466    #[tokio::test]
467    async fn test_bins() {
468        let mut manager = ClipManager::new(":memory:")
469            .await
470            .expect("new should succeed");
471
472        let bin_id = manager.create_bin("Test Bin");
473        let bin = manager.get_bin(&bin_id).expect("get_bin should succeed");
474        assert_eq!(bin.name, "Test Bin");
475
476        let clip = Clip::new(PathBuf::from("/test.mov"));
477        let clip_id = manager
478            .add_clip(clip)
479            .await
480            .expect("add_clip should succeed");
481
482        manager
483            .add_clip_to_bin(&bin_id, clip_id)
484            .expect("add_clip_to_bin should succeed");
485        let bin = manager.get_bin(&bin_id).expect("get_bin should succeed");
486        assert_eq!(bin.count(), 1);
487    }
488
489    #[tokio::test]
490    async fn test_search() {
491        let manager = ClipManager::new(":memory:")
492            .await
493            .expect("new should succeed");
494
495        let mut clip = Clip::new(PathBuf::from("/test.mov"));
496        clip.set_name("Interview");
497        manager
498            .add_clip(clip)
499            .await
500            .expect("operation should succeed");
501
502        let results = manager
503            .search("interview")
504            .await
505            .expect("search should succeed");
506        assert_eq!(results.len(), 1);
507    }
508}