Skip to main content

max / audiofiles

20.0 KB · 591 lines History Blame Raw
1 //! Backend abstraction for database and store access.
2 //!
3 //! The [`Backend`] trait defines all operations that BrowserState needs from
4 //! the data layer. Two implementations exist:
5 //!
6 //! - [`DirectBackend`] — wraps `Mutex<Database>` + `SampleStore`, calls core functions directly.
7 //! Used in tests, standalone mode, and as a reference implementation.
8 //! - `DaemonBackend` (Phase D) — forwards calls to the daemon over Unix socket via JSON-RPC.
9
10 pub mod direct;
11
12 use std::path::{Path, PathBuf};
13
14 use audiofiles_core::analysis::config::AnalysisConfig;
15 use audiofiles_core::analysis::suggest::TagSuggestion;
16 use audiofiles_core::analysis::waveform::WaveformData;
17 use audiofiles_core::analysis::AnalysisResult;
18 use audiofiles_core::edit::EditOperation;
19 use audiofiles_core::export::profile::DeviceProfileSummary;
20 use audiofiles_core::export::{ExportConfig, ExportItem};
21 use audiofiles_core::forge::{ChopMethod, ConformResult, ConformTarget};
22 use audiofiles_core::search::SearchFilter;
23 use audiofiles_core::collections::Collection;
24 use audiofiles_core::vfs::{Vfs, VfsNode, VfsNodeWithAnalysis};
25 use audiofiles_core::{CollectionId, NodeId, VfsId};
26
27 pub use direct::DirectBackend;
28
29 /// Result type for backend operations.
30 pub type BackendResult<T> = Result<T, BackendError>;
31
32 /// Unified error type for backend operations.
33 #[derive(Debug, thiserror::Error)]
34 pub enum BackendError {
35 #[error("{0}")]
36 Core(#[from] audiofiles_core::error::CoreError),
37
38 #[error("{0}")]
39 Other(String),
40 }
41
42 /// Events from long-running background operations (import, analysis, export).
43 ///
44 /// Unifies the separate ImportEvent, WorkerEvent, and ExportEvent types so that
45 /// a single `poll_events()` call can drain all pending worker notifications.
46 #[derive(Debug, serde::Serialize, serde::Deserialize)]
47 pub enum BackendEvent {
48 // Import events
49 ImportWalkProgress {
50 count: usize,
51 total_bytes: u64,
52 },
53 ImportWalkComplete {
54 total: usize,
55 total_bytes: u64,
56 },
57 ImportProgress {
58 completed: usize,
59 total: usize,
60 current_name: String,
61 },
62 ImportFileError {
63 path: String,
64 error: String,
65 },
66 ImportComplete {
67 imported: Vec<(String, String)>,
68 total_files: usize,
69 errors: usize,
70 duplicates: usize,
71 folders: Vec<ImportedFolderDesc>,
72 },
73
74 // Analysis events
75 AnalysisProgress {
76 completed: usize,
77 total: usize,
78 current_name: String,
79 },
80 AnalysisSampleDone {
81 result: Box<AnalysisResult>,
82 suggestions: Vec<TagSuggestion>,
83 },
84 AnalysisSampleError {
85 hash: String,
86 error: String,
87 },
88 AnalysisBatchComplete,
89
90 // Export events
91 ExportProgress {
92 completed: usize,
93 total: usize,
94 current_name: String,
95 },
96 ExportComplete {
97 total: usize,
98 errors: Vec<(String, String)>,
99 },
100
101 // Cleanup events
102 CleanupProgress {
103 completed: usize,
104 total: usize,
105 current_name: String,
106 },
107 CleanupComplete {
108 removed: usize,
109 errors: usize,
110 },
111
112 // Edit events
113 EditStarted {
114 hash: String,
115 },
116 EditComplete {
117 source_hash: String,
118 result_path: std::path::PathBuf,
119 operation: EditOperation,
120 },
121 EditError {
122 hash: String,
123 error: String,
124 },
125 }
126
127 /// Serializable description of an imported folder (for IPC).
128 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
129 pub struct ImportedFolderDesc {
130 pub name: String,
131 pub samples: Vec<(String, String)>,
132 }
133
134 /// Serializable description of an import strategy (for IPC).
135 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
136 pub enum ImportStrategyDesc {
137 Flat { vfs_id: VfsId, parent_id: Option<NodeId> },
138 NewVfs { vfs_name: String },
139 MergeIntoVfs { vfs_id: VfsId, parent_id: Option<NodeId> },
140 }
141
142 /// Serializable description of an export item (for IPC).
143 pub type ExportItemDesc = ExportItem;
144
145 /// Serializable description of an export config (for IPC).
146 pub type ExportConfigDesc = ExportConfig;
147
148 /// Aggregate storage statistics for a vault.
149 #[derive(Debug, Clone, Default)]
150 pub struct StorageStats {
151 pub sample_count: u64,
152 pub total_bytes: u64,
153 pub db_bytes: u64,
154 }
155
156 /// User-config key (in the `user_config` table) for the forge's overshoot
157 /// policy. When the stored value is `"true"`, a conform that overshoots full
158 /// scale at an integer target is trimmed to the ceiling (the gentlest reversible
159 /// fix); otherwise (default) the signal is left untouched and the overshoot is
160 /// only reported, leaving the encoder's clamp as the disclosed last resort.
161 pub const FORGE_AUTO_TRIM_OVERSHOOT_KEY: &str = "forge.auto_trim_overshoot";
162
163 /// The core abstraction separating UI from data access.
164 ///
165 /// Every method is synchronous and blocking. The trait is `Send + Sync` so it
166 /// can live inside `BrowserState` (which must be Send + Sync for nih-plug).
167 pub trait Backend: Send + Sync {
168 // --- VFS ---
169
170 /// List all VFS roots, ordered alphabetically.
171 fn list_vfs(&self) -> BackendResult<Vec<Vfs>>;
172
173 /// Create a new VFS root. Returns the new VFS ID.
174 fn create_vfs(&self, name: &str) -> BackendResult<VfsId>;
175
176 /// Rename a VFS root.
177 fn rename_vfs(&self, id: VfsId, new_name: &str) -> BackendResult<()>;
178
179 /// Delete a VFS root and all its nodes.
180 fn delete_vfs(&self, id: VfsId) -> BackendResult<()>;
181
182 /// List children of a directory with analysis data joined in.
183 fn list_children_enriched(
184 &self,
185 vfs_id: VfsId,
186 parent_id: Option<NodeId>,
187 ) -> BackendResult<Vec<VfsNodeWithAnalysis>>;
188
189 /// List direct children (without analysis data).
190 fn list_children(
191 &self,
192 vfs_id: VfsId,
193 parent_id: Option<NodeId>,
194 ) -> BackendResult<Vec<VfsNode>>;
195
196 /// Create a directory node. Returns the new node ID.
197 fn create_directory(
198 &self,
199 vfs_id: VfsId,
200 parent_id: Option<NodeId>,
201 name: &str,
202 ) -> BackendResult<NodeId>;
203
204 /// Create a sample link node. Returns the new node ID.
205 fn create_sample_link(
206 &self,
207 vfs_id: VfsId,
208 parent_id: Option<NodeId>,
209 name: &str,
210 sample_hash: &str,
211 ) -> BackendResult<NodeId>;
212
213 /// Fetch a single VFS node by ID.
214 fn get_node(&self, id: NodeId) -> BackendResult<VfsNode>;
215
216 /// Walk from a node up to the VFS root, returning root→node path.
217 fn get_breadcrumb(&self, node_id: NodeId) -> BackendResult<Vec<VfsNode>>;
218
219 /// Rename a VFS node.
220 fn rename_node(&self, id: NodeId, new_name: &str) -> BackendResult<()>;
221
222 /// Move a VFS node to a new parent.
223 fn move_node(&self, id: NodeId, new_parent_id: Option<NodeId>) -> BackendResult<()>;
224
225 /// Delete a VFS node (cascades to children).
226 fn delete_node(&self, id: NodeId) -> BackendResult<()>;
227
228 /// Re-insert a previously deleted node (for undo).
229 fn restore_node(&self, node: &VfsNode) -> BackendResult<()>;
230
231 /// Recursively collect a node and all its descendants.
232 fn collect_subtree(&self, node_id: NodeId) -> BackendResult<Vec<VfsNode>>;
233
234 /// List all directories in a VFS with full paths.
235 fn list_all_directories(&self, vfs_id: VfsId) -> BackendResult<Vec<(NodeId, String)>>;
236
237 /// Find VFS nodes by sample hashes within a specific VFS.
238 fn find_nodes_by_hashes(
239 &self,
240 vfs_id: VfsId,
241 hashes: &[&str],
242 ) -> BackendResult<Vec<VfsNodeWithAnalysis>>;
243
244 // --- Tags ---
245
246 /// Add a tag to a sample.
247 fn add_tag(&self, hash: &str, tag: &str) -> BackendResult<()>;
248
249 /// Remove a tag from a sample.
250 fn remove_tag(&self, hash: &str, tag: &str) -> BackendResult<()>;
251
252 /// Get all tags for a sample.
253 fn get_sample_tags(&self, hash: &str) -> BackendResult<Vec<String>>;
254
255 /// List all tags in the database.
256 fn list_all_tags(&self) -> BackendResult<Vec<String>>;
257
258 /// Add a tag to multiple samples. Returns count of tags added.
259 fn bulk_add_tag(&self, hashes: &[&str], tag: &str) -> BackendResult<usize>;
260
261 /// Remove a tag from multiple samples. Returns count of tags removed.
262 fn bulk_remove_tag(&self, hashes: &[&str], tag: &str) -> BackendResult<usize>;
263
264 /// Rename a tag everywhere it appears. Returns count of samples affected.
265 fn rename_tag_globally(&self, old_tag: &str, new_tag: &str) -> BackendResult<usize>;
266
267 /// Count samples that carry an exact tag (used for rename / remove preview).
268 fn count_samples_with_tag(&self, tag: &str) -> BackendResult<usize>;
269
270 /// Remove a tag from every sample that carries it. Returns count of samples affected.
271 fn remove_tag_globally(&self, tag: &str) -> BackendResult<usize>;
272
273 // --- Search ---
274
275 /// Search within a specific VFS folder.
276 fn search_in_folder(
277 &self,
278 filter: &SearchFilter,
279 vfs_id: VfsId,
280 parent_id: Option<NodeId>,
281 ) -> BackendResult<Vec<VfsNodeWithAnalysis>>;
282
283 /// Search globally across all VFS roots.
284 fn search_global(&self, filter: &SearchFilter) -> BackendResult<Vec<VfsNodeWithAnalysis>>;
285
286 // --- Smart folders ---
287
288 // --- Collections ---
289
290 /// List all collections with member counts.
291 fn list_collections(&self) -> BackendResult<Vec<Collection>>;
292
293 /// Create a manual collection. Returns the new ID.
294 fn create_collection(&self, name: &str, description: Option<&str>) -> BackendResult<CollectionId>;
295
296 /// Create a dynamic collection (saved search). Returns the new ID.
297 fn create_dynamic_collection(&self, name: &str, filter: &SearchFilter) -> BackendResult<CollectionId>;
298
299 /// Rename a collection.
300 fn rename_collection(&self, id: CollectionId, new_name: &str) -> BackendResult<()>;
301
302 /// Delete a collection (cascades members).
303 fn delete_collection(&self, id: CollectionId) -> BackendResult<()>;
304
305 /// Add a sample to a collection (no-op if already present).
306 fn add_to_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()>;
307
308 /// Remove a sample from a collection.
309 fn remove_from_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()>;
310
311 /// List sample hashes in a collection.
312 fn list_collection_members(&self, collection_id: CollectionId) -> BackendResult<Vec<String>>;
313
314 /// Get all collections containing a given sample.
315 fn get_sample_collections(&self, sample_hash: &str) -> BackendResult<Vec<Collection>>;
316
317 // --- Analysis ---
318
319 /// Get the full analysis result for a sample, if it exists.
320 fn get_analysis(&self, hash: &str) -> BackendResult<Option<AnalysisResult>>;
321
322 /// Save an analysis result to the database.
323 fn save_analysis(&self, result: &AnalysisResult) -> BackendResult<()>;
324
325 /// Get waveform display data for a sample.
326 fn get_waveform(&self, hash: &str) -> BackendResult<Option<WaveformData>>;
327
328 // --- Similarity ---
329
330 /// Find samples similar to the given hash.
331 fn find_similar(
332 &self,
333 hash: &str,
334 limit: usize,
335 ) -> BackendResult<Vec<audiofiles_core::similarity::SimilarResult>>;
336
337 /// Find near-duplicate samples by fingerprint comparison.
338 fn find_near_duplicates(
339 &self,
340 hash: &str,
341 limit: usize,
342 ) -> BackendResult<Vec<audiofiles_core::fingerprint::DuplicateResult>>;
343
344 // --- Store ---
345
346 /// Import a file into the content-addressed store. Returns the hash.
347 fn import_file(&self, path: &Path) -> BackendResult<String>;
348
349 /// Get the filesystem path for a stored sample.
350 fn sample_path(&self, hash: &str, ext: &str) -> BackendResult<PathBuf>;
351
352 /// Look up the file extension for a sample hash.
353 fn sample_extension(&self, hash: &str) -> BackendResult<String>;
354
355 /// Look up the original filename for a sample hash.
356 fn sample_original_name(&self, hash: &str) -> BackendResult<String>;
357
358 /// Remove a sample from the store and database (CASCADE handles VFS nodes, tags, analysis).
359 fn remove_sample(&self, hash: &str) -> BackendResult<()>;
360
361 /// Remove samples no longer referenced by any VFS node. Returns count removed.
362 fn remove_orphaned_samples(&self) -> BackendResult<usize>;
363
364 /// User-initiated local orphan cleanup. Wraps `remove_orphaned_samples`
365 /// with sync trigger suppression (`applying_remote='1'`) so the cleanup
366 /// stays local to this device — other devices keep their own copies of
367 /// the samples until they independently determine the samples are
368 /// orphaned. Forward-compatible with the planned tombstone-based
369 /// sample-deletion design (see `docs/design-sample-deletion.md`).
370 fn cleanup_orphans_local(&self) -> BackendResult<usize>;
371
372 /// Look up the source_path for an loose-files mode sample. Returns None for normal samples.
373 fn sample_source_path(&self, hash: &str) -> BackendResult<Option<String>>;
374
375 /// Relocate an loose-files mode sample to a new path (verifies hash match).
376 fn relocate_sample(&self, hash: &str, new_path: &Path) -> BackendResult<()>;
377
378 /// Check integrity of loose-files mode samples. Returns (valid, missing).
379 fn check_vault_integrity(&self) -> BackendResult<(usize, usize)>;
380
381 /// Delete all loose-files mode samples whose source files are missing. Returns count purged.
382 fn purge_missing_loose_files(&self) -> BackendResult<usize>;
383
384 /// Search `search_root` for files matching missing loose-files samples by hash;
385 /// update their source_path to the new location. Returns
386 /// `(relocated, still_missing)` so the caller can decide whether to prompt
387 /// again with a different directory.
388 fn relocate_missing_loose_files(
389 &self,
390 search_root: &std::path::Path,
391 ) -> BackendResult<(usize, usize)>;
392
393 // --- Export ---
394
395 /// Collect export items from a VFS subtree.
396 fn collect_export_items(
397 &self,
398 vfs_id: VfsId,
399 parent_id: Option<NodeId>,
400 ) -> BackendResult<Vec<ExportItem>>;
401
402 /// Populate tags on export items.
403 fn enrich_export_with_tags(&self, items: &mut [ExportItem]) -> BackendResult<()>;
404
405 // --- Device profiles ---
406
407 /// List available device profiles for device-aware export.
408 fn list_device_profiles(&self) -> BackendResult<Vec<DeviceProfileSummary>>;
409
410 /// Resolve a device profile into a conform target for a source of the given
411 /// sample rate. Returns `None` when the profile isn't found (or device
412 /// profiles are unavailable in this build).
413 fn device_conform_target(
414 &self,
415 profile_name: &str,
416 source_rate: u32,
417 ) -> BackendResult<Option<ConformTarget>>;
418
419 // --- Sample Forge ---
420
421 /// Compute slice-boundary positions (normalized 0..1 fractions of total
422 /// length) for the given chop method, for waveform overlay preview.
423 fn compute_chop_preview(
424 &self,
425 hash: &str,
426 ext: &str,
427 method: &ChopMethod,
428 ) -> BackendResult<Vec<f32>>;
429
430 /// Chop a sample into slices written as new samples in a `"{name}_slices"`
431 /// directory under `parent_id`. Returns the number of slices created.
432 fn chop_sample(
433 &self,
434 vfs_id: VfsId,
435 hash: &str,
436 ext: &str,
437 name: &str,
438 parent_id: Option<NodeId>,
439 method: &ChopMethod,
440 ) -> BackendResult<usize>;
441
442 /// Conform a sample to `target`, writing the result as a new sibling sample
443 /// under `parent_id`. The result carries the new sample's hash plus any
444 /// true-peak overshoot the conform surfaced (see [`ConformResult`]); whether
445 /// an overshoot is trimmed or left for the encoder follows the
446 /// [`FORGE_AUTO_TRIM_OVERSHOOT_KEY`] user-config toggle.
447 fn conform_sample(
448 &self,
449 vfs_id: VfsId,
450 hash: &str,
451 ext: &str,
452 name: &str,
453 parent_id: Option<NodeId>,
454 target: &ConformTarget,
455 ) -> BackendResult<ConformResult>;
456
457 // --- Config ---
458
459 /// Get a user config value by key.
460 fn get_config(&self, key: &str) -> BackendResult<Option<String>>;
461
462 /// Set a user config value.
463 fn set_config(&self, key: &str, value: &str) -> BackendResult<()>;
464
465 /// Delete a user config value by key. No-op if the key does not exist.
466 fn delete_config(&self, key: &str) -> BackendResult<()>;
467
468 /// Set whether a VFS should sync audio file blobs to cloud.
469 fn set_vfs_sync_files(&self, id: VfsId, enabled: bool) -> BackendResult<()>;
470
471 /// Get whether a VFS has audio file blob syncing enabled.
472 fn get_vfs_sync_files(&self, id: VfsId) -> BackendResult<bool>;
473
474 // --- VFS Mirror ---
475
476 /// Synchronise the VFS mirror directory with the current VFS state.
477 /// Returns `(dirs_created, links_created, entries_removed)`.
478 fn sync_vfs_mirror(&self, mirror_root: &Path) -> BackendResult<(usize, usize, usize)>;
479
480 // --- Long-running operations ---
481
482 /// Start a folder import in the background.
483 fn start_import(
484 &self,
485 source: &Path,
486 strategy: ImportStrategyDesc,
487 ) -> BackendResult<()>;
488
489 /// Start analysis on a batch of samples.
490 fn start_analysis(
491 &self,
492 samples: Vec<(String, String)>,
493 config: AnalysisConfig,
494 ) -> BackendResult<()>;
495
496 /// Start an export operation.
497 fn start_export(
498 &self,
499 items: Vec<ExportItemDesc>,
500 config: ExportConfigDesc,
501 ) -> BackendResult<()>;
502
503 /// Cancel a running import.
504 fn cancel_import(&self) -> BackendResult<()>;
505
506 /// Cancel a running analysis.
507 fn cancel_analysis(&self) -> BackendResult<()>;
508
509 /// Cancel a running export.
510 fn cancel_export(&self) -> BackendResult<()>;
511
512 /// Start an edit operation on a sample.
513 fn start_edit(&self, hash: &str, ext: &str, operation: EditOperation) -> BackendResult<()>;
514
515 /// Cancel a running edit.
516 fn cancel_edit(&self) -> BackendResult<()>;
517
518 /// Start background orphaned sample cleanup.
519 fn start_cleanup(&self) -> BackendResult<()>;
520
521 /// Cancel a running cleanup.
522 fn cancel_cleanup(&self) -> BackendResult<()>;
523
524 /// Record an edit in the edit_history table.
525 fn record_edit_history(
526 &self,
527 source_hash: &str,
528 result_hash: &str,
529 operation: &EditOperation,
530 ) -> BackendResult<()>;
531
532 /// Delete the most recent `edit_history` row matching this (source, result)
533 /// pair. Used by `BrowserState::undo_last_edit` to reverse a previously
534 /// recorded edit when the user clicks the inline Undo affordance.
535 fn delete_edit_history(
536 &self,
537 source_hash: &str,
538 result_hash: &str,
539 ) -> BackendResult<()>;
540
541 /// Get aggregate storage statistics for the current vault.
542 fn storage_stats(&self) -> BackendResult<StorageStats>;
543
544 /// Per-VFS storage stats: `(unique_sample_count, total_bytes)`. Surfaced in
545 /// the sync panel's per-VFS toggle rows so the user can see upload size
546 /// before enabling blob sync for that vault.
547 fn vfs_storage_stats(&self, vfs_id: audiofiles_core::VfsId) -> BackendResult<(u64, u64)>;
548
549 /// Non-blocking poll for worker events.
550 fn poll_events(&self) -> Vec<BackendEvent>;
551 }
552
553 #[cfg(test)]
554 mod tests {
555 use super::*;
556
557 #[test]
558 fn backend_error_from_core() {
559 let core_err = audiofiles_core::error::CoreError::NodeNotFound(NodeId::from(42));
560 let backend_err: BackendError = core_err.into();
561 assert!(backend_err.to_string().contains("42"));
562 }
563
564 #[test]
565 fn backend_error_other() {
566 let err = BackendError::Other("test error".to_string());
567 assert_eq!(err.to_string(), "test error");
568 }
569
570 #[test]
571 fn imported_folder_desc_serializes() {
572 let desc = ImportedFolderDesc {
573 name: "Drums".to_string(),
574 samples: vec![("hash1".to_string(), "wav".to_string())],
575 };
576 let json = serde_json::to_string(&desc).unwrap();
577 assert!(json.contains("Drums"));
578 }
579
580 #[test]
581 fn import_strategy_desc_variants() {
582 let flat = ImportStrategyDesc::Flat { vfs_id: VfsId::from(1), parent_id: None };
583 let json = serde_json::to_string(&flat).unwrap();
584 assert!(json.contains("Flat"));
585
586 let new_vfs = ImportStrategyDesc::NewVfs { vfs_name: "Test".to_string() };
587 let json = serde_json::to_string(&new_vfs).unwrap();
588 assert!(json.contains("Test"));
589 }
590 }
591