1use 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
17pub 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 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 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 pub async fn get_clip(&self, clip_id: &ClipId) -> ClipResult<Clip> {
72 self.database.get_clip(clip_id).await
73 }
74
75 pub async fn update_clip(&self, clip: Clip) -> ClipResult<()> {
81 self.database.save_clip(&clip).await
82 }
83
84 pub async fn delete_clip(&self, clip_id: &ClipId) -> ClipResult<()> {
90 self.database.delete_clip(clip_id).await
91 }
92
93 pub async fn get_all_clips(&self) -> ClipResult<Vec<Clip>> {
99 self.database.get_all_clips().await
100 }
101
102 pub async fn clip_count(&self) -> ClipResult<i64> {
108 self.database.count_clips().await
109 }
110
111 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 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 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 pub async fn search(&self, query: &str) -> ClipResult<Vec<Clip>> {
159 self.database.search_clips(query).await
160 }
161
162 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 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 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 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 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 #[must_use]
217 pub fn list_bins(&self) -> Vec<&Bin> {
218 self.bins.values().collect()
219 }
220
221 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 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 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 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 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 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 pub fn create_smart_collection(&mut self, smart_collection: SmartCollection) {
297 self.smart_collections.push(smart_collection);
298 }
299
300 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 pub fn add_marker(&mut self, clip_id: ClipId, marker: Marker) {
319 self.marker_manager.add_marker(clip_id, marker);
320 }
321
322 #[must_use]
324 pub fn get_markers(&self, clip_id: &ClipId) -> Vec<&Marker> {
325 self.marker_manager.get_markers(clip_id)
326 }
327
328 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 pub fn add_take(&mut self, take: Take) {
341 self.take_manager.add_take(take);
342 }
343
344 #[must_use]
346 pub fn get_scene_takes(&self, scene: &str) -> Vec<&Take> {
347 self.take_manager.get_scene_takes(scene)
348 }
349
350 #[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 pub fn add_proxy(&mut self, link: ProxyLink) {
360 self.proxy_manager.add_link(link);
361 }
362
363 #[must_use]
365 pub fn get_proxies(&self, clip_id: &ClipId) -> Vec<&ProxyLink> {
366 self.proxy_manager.get_links(clip_id)
367 }
368
369 #[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 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 #[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 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 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}