//! `DirectBackend` Sample Forge: chop and conform, in both the synchronous form //! and the worker-backed form that streams progress back to the UI. use super::{ BackendError, BackendResult, ChopMethod, ConformResult, ConformTarget, DirectBackend, ForgeBackend, ForgeCommand, ForgePending, NodeId, VfsId, instrument, }; impl ForgeBackend for DirectBackend { // --- Sample Forge --- #[instrument(skip_all)] fn start_chop_preview(&self, hash: &str, ext: &str, method: &ChopMethod) -> BackendResult<()> { // Resolve the source path under a brief lock; the decode + slice runs on // the forge worker and the marks arrive via ForgeEvent::PreviewComplete. let path = { let db = self.db.lock(); audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)? }; // Reuse the forge worker if present (preview queues behind any running // chop/conform); spawn one lazily otherwise. Preview is read-only and // sets no forge_pending, so it never interferes with an import. let mut guard = self.forge_worker.lock(); if guard.is_none() { let handle = audiofiles_core::forge::spawn_forge_worker() .map_err(|e| BackendError::Other(format!("failed to spawn forge worker: {e}")))?; *guard = Some(handle); } let delivered = guard.as_ref().is_some_and(|w| { w.send(ForgeCommand::Preview { source_path: path, method: method.clone(), }) }); // Dead cached worker → drop it (next call respawns) and report Err so the // caller doesn't set forge.busy and wedge the repaint guard / busy-guard. if !delivered { *guard = None; return Err(BackendError::Other( "forge worker is not running".to_string(), )); } Ok(()) } #[instrument(skip_all)] fn chop_sample( &self, vfs_id: VfsId, hash: &str, ext: &str, name: &str, parent_id: Option, method: &ChopMethod, ) -> BackendResult { let db = self.db.lock(); let path = audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)?; let result = audiofiles_core::forge::chop_to_vfs( &self.store, &db, vfs_id, &path, name, parent_id, method, )?; Ok(result.slice_count) } #[instrument(skip_all)] fn conform_sample( &self, vfs_id: VfsId, hash: &str, ext: &str, name: &str, parent_id: Option, target: &ConformTarget, ) -> BackendResult { let db = self.db.lock(); // Overshoot policy: trim to the ceiling only when the user has opted in; // the default leaves the signal untouched and merely reports. let auto_trim = db .conn() .query_row( "SELECT value FROM user_config WHERE key = ?1", [crate::backend::FORGE_AUTO_TRIM_OVERSHOOT_KEY], |row| row.get::<_, String>(0), ) .ok() .is_some_and(|v| v == "true"); let path = audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)?; Ok(audiofiles_core::forge::conform_to_vfs( &self.store, &db, vfs_id, &path, name, parent_id, target, auto_trim, )?) } #[instrument(skip_all)] fn start_chop( &self, vfs_id: VfsId, hash: &str, ext: &str, name: &str, parent_id: Option, method: &ChopMethod, ) -> BackendResult<()> { // Resolve the source path under a brief lock; the heavy work runs on the // worker, and the import lands when ForgeEvent::ChopComplete is drained. let path = { let db = self.db.lock(); audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)? }; // Reuse the persistent worker. Sending Cancel sets the cancel flag so any // in-flight job bails cooperatively; the queued Chop then runs. Dropping // the old handle here would join its thread ON the GUI thread, a visible // frame stall on a long in-flight job. The worker resets the flag when it // dequeues the new command. { let mut guard = self.forge_worker.lock(); if guard.is_none() { *guard = Some(audiofiles_core::forge::spawn_forge_worker().map_err(|e| { BackendError::Other(format!("failed to spawn forge worker: {e}")) })?); } let handle = guard.as_ref().expect("forge worker present"); let _ = handle.send(ForgeCommand::Cancel); let delivered = handle.send(ForgeCommand::Chop { source_path: path, base_name: name.to_string(), method: method.clone(), }); if !delivered { *guard = None; return Err(BackendError::Other( "forge worker is not running".to_string(), )); } } *self.forge_pending.lock() = Some(ForgePending::Chop { vfs_id, parent_id }); Ok(()) } #[instrument(skip_all)] fn start_conform( &self, vfs_id: VfsId, hash: &str, ext: &str, name: &str, parent_id: Option, target: &ConformTarget, ) -> BackendResult<()> { let (path, auto_trim) = { let db = self.db.lock(); let auto_trim = db .conn() .query_row( "SELECT value FROM user_config WHERE key = ?1", [crate::backend::FORGE_AUTO_TRIM_OVERSHOOT_KEY], |row| row.get::<_, String>(0), ) .ok() .is_some_and(|v| v == "true"); let path = audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)?; (path, auto_trim) }; // Reuse the persistent worker (see start_chop): Cancel + queue, never // drop+join on the GUI thread. { let mut guard = self.forge_worker.lock(); if guard.is_none() { *guard = Some(audiofiles_core::forge::spawn_forge_worker().map_err(|e| { BackendError::Other(format!("failed to spawn forge worker: {e}")) })?); } let handle = guard.as_ref().expect("forge worker present"); let _ = handle.send(ForgeCommand::Cancel); let delivered = handle.send(ForgeCommand::Conform { source_path: path, base_name: name.to_string(), target: target.clone(), auto_trim, }); if !delivered { *guard = None; return Err(BackendError::Other( "forge worker is not running".to_string(), )); } } *self.forge_pending.lock() = Some(ForgePending::Conform { vfs_id, parent_id, sample_rate: target.sample_rate, bit_depth: target.bit_depth, }); Ok(()) } }