max / audiofiles
142 files changed,
+7928 insertions,
-4102 deletions
| @@ -3,7 +3,7 @@ | |||
| 3 | 3 | use audiofiles_browser::ui::theme; | |
| 4 | 4 | use eframe::egui; | |
| 5 | 5 | ||
| 6 | - | use super::{AudioFilesApp, AppScreen, SYNC_SERVER_URL}; | |
| 6 | + | use super::{AppScreen, AudioFilesApp, SYNC_SERVER_URL}; | |
| 7 | 7 | ||
| 8 | 8 | impl AudioFilesApp { | |
| 9 | 9 | /// Draw the license activation screen. | |
| @@ -103,8 +103,15 @@ impl AudioFilesApp { | |||
| 103 | 103 | ||
| 104 | 104 | let can_activate = !self.activating && !self.license_key_input.trim().is_empty(); | |
| 105 | 105 | ui.horizontal(|ui| { | |
| 106 | - | let button_text = if self.activating { "Activating\u{2026}" } else { "Activate" }; | |
| 107 | - | if ui.add_enabled(can_activate, egui::Button::new(button_text)).clicked() { | |
| 106 | + | let button_text = if self.activating { | |
| 107 | + | "Activating\u{2026}" | |
| 108 | + | } else { | |
| 109 | + | "Activate" | |
| 110 | + | }; | |
| 111 | + | if ui | |
| 112 | + | .add_enabled(can_activate, egui::Button::new(button_text)) | |
| 113 | + | .clicked() | |
| 114 | + | { | |
| 108 | 115 | self.start_activation(); | |
| 109 | 116 | } | |
| 110 | 117 | if self.activating { | |
| @@ -139,10 +146,7 @@ impl AudioFilesApp { | |||
| 139 | 146 | } | |
| 140 | 147 | ||
| 141 | 148 | ui.add_space(theme::space::MD); | |
| 142 | - | ui.hyperlink_to( | |
| 143 | - | "Get a license key", | |
| 144 | - | "https://makenot.work/store/audiofiles", | |
| 145 | - | ); | |
| 149 | + | ui.hyperlink_to("Get a license key", "https://makenot.work/store/audiofiles"); | |
| 146 | 150 | ||
| 147 | 151 | ui.add_space(theme::space::XL); | |
| 148 | 152 | ui.horizontal(|ui| { | |
| @@ -196,16 +200,15 @@ impl AudioFilesApp { | |||
| 196 | 200 | browser.settings.license_key_masked = Some(mask_key(&cache.key_code)); | |
| 197 | 201 | browser.settings.trial_days_remaining = None; | |
| 198 | 202 | } else if let Some(ref trial) = self.trial_state { | |
| 199 | - | browser.settings.trial_days_remaining = Some(super::license::trial_days_remaining(trial)); | |
| 203 | + | browser.settings.trial_days_remaining = | |
| 204 | + | Some(super::license::trial_days_remaining(trial)); | |
| 200 | 205 | } | |
| 201 | 206 | let mid = &self.machine_id; | |
| 202 | - | browser.settings.machine_id = Some( | |
| 203 | - | if mid.len() > 12 { | |
| 204 | - | format!("{}...{}", &mid[..8], &mid[mid.len()-4..]) | |
| 205 | - | } else { | |
| 206 | - | mid.clone() | |
| 207 | - | } | |
| 208 | - | ); | |
| 207 | + | browser.settings.machine_id = Some(if mid.len() > 12 { | |
| 208 | + | format!("{}...{}", &mid[..8], &mid[mid.len() - 4..]) | |
| 209 | + | } else { | |
| 210 | + | mid.clone() | |
| 211 | + | }); | |
| 209 | 212 | } | |
| 210 | 213 | } | |
| 211 | 214 | ||
| @@ -289,8 +292,8 @@ pub(crate) fn trial_button_label(trial: Option<&super::license::TrialState>) -> | |||
| 289 | 292 | ||
| 290 | 293 | #[cfg(test)] | |
| 291 | 294 | mod tests { | |
| 292 | - | use super::*; | |
| 293 | 295 | use super::super::license::{ActivationError, TrialState}; | |
| 296 | + | use super::*; | |
| 294 | 297 | ||
| 295 | 298 | fn trial_starting_days_ago(days: i64) -> TrialState { | |
| 296 | 299 | let start = chrono::Utc::now() - chrono::Duration::days(days); | |
| @@ -304,12 +307,18 @@ mod tests { | |||
| 304 | 307 | ||
| 305 | 308 | #[test] | |
| 306 | 309 | fn network_error_offers_retry() { | |
| 307 | - | assert_eq!(recovery_affordance(&ActivationError::Network), RecoveryAffordance::Retry); | |
| 310 | + | assert_eq!( | |
| 311 | + | recovery_affordance(&ActivationError::Network), | |
| 312 | + | RecoveryAffordance::Retry | |
| 313 | + | ); | |
| 308 | 314 | } | |
| 309 | 315 | ||
| 310 | 316 | #[test] | |
| 311 | 317 | fn server_error_offers_retry() { | |
| 312 | - | assert_eq!(recovery_affordance(&ActivationError::Server(503)), RecoveryAffordance::Retry); | |
| 318 | + | assert_eq!( | |
| 319 | + | recovery_affordance(&ActivationError::Server(503)), | |
| 320 | + | RecoveryAffordance::Retry | |
| 321 | + | ); | |
| 313 | 322 | } | |
| 314 | 323 | ||
| 315 | 324 | #[test] |
| @@ -133,7 +133,10 @@ mod tests { | |||
| 133 | 133 | fn read_key_file_trims_whitespace() { | |
| 134 | 134 | let dir = tempfile::tempdir().unwrap(); | |
| 135 | 135 | std::fs::write(dir.path().join("sync_api_key"), " key-with-spaces \n").unwrap(); | |
| 136 | - | assert_eq!(read_key_file(dir.path()), Some("key-with-spaces".to_string())); | |
| 136 | + | assert_eq!( | |
| 137 | + | read_key_file(dir.path()), | |
| 138 | + | Some("key-with-spaces".to_string()) | |
| 139 | + | ); | |
| 137 | 140 | } | |
| 138 | 141 | ||
| 139 | 142 | #[test] |
| @@ -5,11 +5,11 @@ use std::sync::Arc; | |||
| 5 | 5 | use audiofiles_browser::instrument::render_voices; | |
| 6 | 6 | use audiofiles_browser::preview::PreviewPlayback; | |
| 7 | 7 | use audiofiles_browser::state::SharedState; | |
| 8 | - | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; | |
| 9 | 8 | use cpal::Stream; | |
| 9 | + | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; | |
| 10 | 10 | use parking_lot::Mutex; | |
| 11 | - | use tracing::instrument; | |
| 12 | 11 | use thiserror::Error; | |
| 12 | + | use tracing::instrument; | |
| 13 | 13 | ||
| 14 | 14 | /// Errors from audio output stream setup. | |
| 15 | 15 | #[derive(Error, Debug)] | |
| @@ -33,9 +33,7 @@ pub enum AudioError { | |||
| 33 | 33 | #[instrument(skip_all)] | |
| 34 | 34 | pub fn start_output_stream(shared: Arc<SharedState>) -> Result<(Stream, u32, String), AudioError> { | |
| 35 | 35 | let host = cpal::default_host(); | |
| 36 | - | let device = host | |
| 37 | - | .default_output_device() | |
| 38 | - | .ok_or(AudioError::NoDevice)?; | |
| 36 | + | let device = host.default_output_device().ok_or(AudioError::NoDevice)?; | |
| 39 | 37 | ||
| 40 | 38 | let device_name = device | |
| 41 | 39 | .description() | |
| @@ -98,47 +96,46 @@ fn build_stream<T: cpal::SizedSample + cpal::FromSample<f32>>( | |||
| 98 | 96 | // recovers instead of going permanently silent. | |
| 99 | 97 | let err_shared = Arc::clone(&shared); | |
| 100 | 98 | ||
| 101 | - | let stream = device | |
| 102 | - | .build_output_stream( | |
| 103 | - | config, | |
| 104 | - | move |data: &mut [T], _: &cpal::OutputCallbackInfo| { | |
| 105 | - | let num_samples = data.len(); | |
| 106 | - | ||
| 107 | - | // Backstop for an unexpectedly large block (pre-sized above to | |
| 108 | - | // avoid allocating here on the steady-state path). | |
| 109 | - | if mix_buf.len() < num_samples { | |
| 110 | - | mix_buf.resize(num_samples, 0.0); | |
| 111 | - | } | |
| 112 | - | let buf = &mut mix_buf[..num_samples]; | |
| 113 | - | ||
| 114 | - | // Zero the mix buffer | |
| 115 | - | for s in buf.iter_mut() { | |
| 116 | - | *s = 0.0; | |
| 117 | - | } | |
| 118 | - | ||
| 119 | - | // Fill preview audio | |
| 120 | - | fill_preview(&shared.preview, buf, channels, device_sample_rate); | |
| 121 | - | ||
| 122 | - | // Fill instrument audio (additive) | |
| 123 | - | if let Some(mut inst) = shared.instrument.try_lock() { | |
| 124 | - | render_voices(&mut inst, buf, channels, device_sample_rate); | |
| 125 | - | } | |
| 126 | - | ||
| 127 | - | // Convert f32 mix → output format with clamp | |
| 128 | - | for (out, &mix) in data.iter_mut().zip(buf.iter()) { | |
| 129 | - | *out = T::from_sample(mix.clamp(-1.0, 1.0)); | |
| 130 | - | } | |
| 131 | - | }, | |
| 132 | - | move |err| { | |
| 133 | - | tracing::error!("audio stream error: {err}"); | |
| 134 | - | // Signal the app loop to rebuild the stream. Lock-free store only | |
| 135 | - | // — this runs in cpal's error context, never take a lock here. | |
| 136 | - | err_shared | |
| 137 | - | .device_lost | |
| 138 | - | .store(true, std::sync::atomic::Ordering::Relaxed); | |
| 139 | - | }, | |
| 140 | - | None, | |
| 141 | - | )?; | |
| 99 | + | let stream = device.build_output_stream( | |
| 100 | + | config, | |
| 101 | + | move |data: &mut [T], _: &cpal::OutputCallbackInfo| { | |
| 102 | + | let num_samples = data.len(); | |
| 103 | + | ||
| 104 | + | // Backstop for an unexpectedly large block (pre-sized above to | |
| 105 | + | // avoid allocating here on the steady-state path). | |
| 106 | + | if mix_buf.len() < num_samples { | |
| 107 | + | mix_buf.resize(num_samples, 0.0); | |
| 108 | + | } | |
| 109 | + | let buf = &mut mix_buf[..num_samples]; | |
| 110 | + | ||
| 111 | + | // Zero the mix buffer | |
| 112 | + | for s in buf.iter_mut() { | |
| 113 | + | *s = 0.0; | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | // Fill preview audio | |
| 117 | + | fill_preview(&shared.preview, buf, channels, device_sample_rate); | |
| 118 | + | ||
| 119 | + | // Fill instrument audio (additive) | |
| 120 | + | if let Some(mut inst) = shared.instrument.try_lock() { | |
| 121 | + | render_voices(&mut inst, buf, channels, device_sample_rate); | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | // Convert f32 mix → output format with clamp | |
| 125 | + | for (out, &mix) in data.iter_mut().zip(buf.iter()) { | |
| 126 | + | *out = T::from_sample(mix.clamp(-1.0, 1.0)); | |
| 127 | + | } | |
| 128 | + | }, | |
| 129 | + | move |err| { | |
| 130 | + | tracing::error!("audio stream error: {err}"); | |
| 131 | + | // Signal the app loop to rebuild the stream. Lock-free store only | |
| 132 | + | // — this runs in cpal's error context, never take a lock here. | |
| 133 | + | err_shared | |
| 134 | + | .device_lost | |
| 135 | + | .store(true, std::sync::atomic::Ordering::Relaxed); | |
| 136 | + | }, | |
| 137 | + | None, | |
| 138 | + | )?; | |
| 142 | 139 | Ok(stream) | |
| 143 | 140 | } | |
| 144 | 141 | ||
| @@ -327,10 +324,7 @@ mod tests { | |||
| 327 | 324 | ||
| 328 | 325 | #[test] | |
| 329 | 326 | fn fill_cpal_resamples_96k_to_48k() { | |
| 330 | - | let playback = make_playback( | |
| 331 | - | vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.5, 0.5], | |
| 332 | - | 96000, true, | |
| 333 | - | ); | |
| 327 | + | let playback = make_playback(vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.5, 0.5], 96000, true); | |
| 334 | 328 | let mut data = vec![0.0f32; 4]; | |
| 335 | 329 | ||
| 336 | 330 | fill_cpal_output(&playback, &mut data, 2, 48000); |
| @@ -98,8 +98,7 @@ pub fn load_license(data_dir: &Path) -> LicenseStatus { | |||
| 98 | 98 | pub fn save_license(data_dir: &Path, cache: &LicenseCache) -> io::Result<()> { | |
| 99 | 99 | let path = data_dir.join("license.json"); | |
| 100 | 100 | let tmp = data_dir.join("license.json.tmp"); | |
| 101 | - | let json = serde_json::to_string_pretty(cache) | |
| 102 | - | .map_err(io::Error::other)?; | |
| 101 | + | let json = serde_json::to_string_pretty(cache).map_err(io::Error::other)?; | |
| 103 | 102 | std::fs::write(&tmp, &json)?; | |
| 104 | 103 | std::fs::rename(&tmp, &path) | |
| 105 | 104 | } | |
| @@ -134,10 +133,22 @@ pub enum ActivationError { | |||
| 134 | 133 | impl std::fmt::Display for ActivationError { | |
| 135 | 134 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 136 | 135 | match self { | |
| 137 | - | Self::Network => write!(f, "Couldn't reach the activation server. Check your connection and try again."), | |
| 138 | - | Self::Server(code) => write!(f, "The activation server returned an error ({code}). Try again in a few minutes."), | |
| 139 | - | Self::InvalidKey => write!(f, "We didn't recognise that key. Double-check spelling, or get a new one."), | |
| 140 | - | Self::MachineLimit => write!(f, "This key is already in use on another machine. Deactivate it there first."), | |
| 136 | + | Self::Network => write!( | |
| 137 | + | f, | |
| 138 | + | "Couldn't reach the activation server. Check your connection and try again." | |
| 139 | + | ), | |
| 140 | + | Self::Server(code) => write!( | |
| 141 | + | f, | |
| 142 | + | "The activation server returned an error ({code}). Try again in a few minutes." | |
| 143 | + | ), | |
| 144 | + | Self::InvalidKey => write!( | |
| 145 | + | f, | |
| 146 | + | "We didn't recognise that key. Double-check spelling, or get a new one." | |
| 147 | + | ), | |
| 148 | + | Self::MachineLimit => write!( | |
| 149 | + | f, | |
| 150 | + | "This key is already in use on another machine. Deactivate it there first." | |
| 151 | + | ), | |
| 141 | 152 | Self::Other(msg) => write!(f, "{msg}"), | |
| 142 | 153 | } | |
| 143 | 154 | } | |
| @@ -151,7 +162,8 @@ fn classify_server_error(msg: &str) -> ActivationError { | |||
| 151 | 162 | let lower = msg.to_lowercase(); | |
| 152 | 163 | if lower.contains("machine") || lower.contains("already activated") || lower.contains("limit") { | |
| 153 | 164 | ActivationError::MachineLimit | |
| 154 | - | } else if lower.contains("invalid") || lower.contains("not found") || lower.contains("unknown") { | |
| 165 | + | } else if lower.contains("invalid") || lower.contains("not found") || lower.contains("unknown") | |
| 166 | + | { | |
| 155 | 167 | ActivationError::InvalidKey | |
| 156 | 168 | } else { | |
| 157 | 169 | ActivationError::Other(msg.to_string()) | |
| @@ -163,7 +175,11 @@ fn classify_server_error(msg: &str) -> ActivationError { | |||
| 163 | 175 | /// Sends the key and machine ID to the server for validation. On success the | |
| 164 | 176 | /// server records an activation slot; on failure the returned error is | |
| 165 | 177 | /// classified for per-class UI messaging. | |
| 166 | - | pub async fn activate_key(server_url: &str, key: &str, machine_id: &str) -> Result<(), ActivationError> { | |
| 178 | + | pub async fn activate_key( | |
| 179 | + | server_url: &str, | |
| 180 | + | key: &str, | |
| 181 | + | machine_id: &str, | |
| 182 | + | ) -> Result<(), ActivationError> { | |
| 167 | 183 | let client = reqwest::Client::builder() | |
| 168 | 184 | .timeout(std::time::Duration::from_secs(15)) | |
| 169 | 185 | .build() | |
| @@ -176,18 +192,13 @@ pub async fn activate_key(server_url: &str, key: &str, machine_id: &str) -> Resu | |||
| 176 | 192 | label: None, | |
| 177 | 193 | }; | |
| 178 | 194 | ||
| 179 | - | let resp = client | |
| 180 | - | .post(&url) | |
| 181 | - | .json(&body) | |
| 182 | - | .send() | |
| 183 | - | .await | |
| 184 | - | .map_err(|e| { | |
| 185 | - | if e.is_timeout() || e.is_connect() || e.is_request() { | |
| 186 | - | ActivationError::Network | |
| 187 | - | } else { | |
| 188 | - | ActivationError::Other(format!("Network error: {e}")) | |
| 189 | - | } | |
| 190 | - | })?; | |
| 195 | + | let resp = client.post(&url).json(&body).send().await.map_err(|e| { | |
| 196 | + | if e.is_timeout() || e.is_connect() || e.is_request() { | |
| 197 | + | ActivationError::Network | |
| 198 | + | } else { | |
| 199 | + | ActivationError::Other(format!("Network error: {e}")) | |
| 200 | + | } | |
| 201 | + | })?; | |
| 191 | 202 | ||
| 192 | 203 | if !resp.status().is_success() { | |
| 193 | 204 | return Err(ActivationError::Server(resp.status().as_u16())); | |
| @@ -201,7 +212,9 @@ pub async fn activate_key(server_url: &str, key: &str, machine_id: &str) -> Resu | |||
| 201 | 212 | if parsed.valid { | |
| 202 | 213 | Ok(()) | |
| 203 | 214 | } else { | |
| 204 | - | let msg = parsed.error.unwrap_or_else(|| "Invalid license key".to_string()); | |
| 215 | + | let msg = parsed | |
| 216 | + | .error | |
| 217 | + | .unwrap_or_else(|| "Invalid license key".to_string()); | |
| 205 | 218 | Err(classify_server_error(&msg)) | |
| 206 | 219 | } | |
| 207 | 220 | } | |
| @@ -222,7 +235,11 @@ pub enum DeactivationError { | |||
| 222 | 235 | } | |
| 223 | 236 | ||
| 224 | 237 | /// Deactivate a license key (best-effort, fire-and-forget). | |
| 225 | - | pub async fn deactivate_key(server_url: &str, key: &str, machine_id: &str) -> Result<(), DeactivationError> { | |
| 238 | + | pub async fn deactivate_key( | |
| 239 | + | server_url: &str, | |
| 240 | + | key: &str, | |
| 241 | + | machine_id: &str, | |
| 242 | + | ) -> Result<(), DeactivationError> { | |
| 226 | 243 | let client = reqwest::Client::builder() | |
| 227 | 244 | .timeout(std::time::Duration::from_secs(15)) | |
| 228 | 245 | .build() | |
| @@ -369,14 +386,20 @@ mod tests { | |||
| 369 | 386 | #[test] | |
| 370 | 387 | fn load_missing_returns_unlicensed() { | |
| 371 | 388 | let dir = tempfile::tempdir().unwrap(); | |
| 372 | - | assert!(matches!(load_license(dir.path()), LicenseStatus::Unlicensed)); | |
| 389 | + | assert!(matches!( | |
| 390 | + | load_license(dir.path()), | |
| 391 | + | LicenseStatus::Unlicensed | |
| 392 | + | )); | |
| 373 | 393 | } | |
| 374 | 394 | ||
| 375 | 395 | #[test] | |
| 376 | 396 | fn load_corrupt_returns_unlicensed() { | |
| 377 | 397 | let dir = tempfile::tempdir().unwrap(); | |
| 378 | 398 | std::fs::write(dir.path().join("license.json"), "not json{{{").unwrap(); | |
| 379 | - | assert!(matches!(load_license(dir.path()), LicenseStatus::Unlicensed)); | |
| 399 | + | assert!(matches!( | |
| 400 | + | load_license(dir.path()), | |
| 401 | + | LicenseStatus::Unlicensed | |
| 402 | + | )); | |
| 380 | 403 | } | |
| 381 | 404 | ||
| 382 | 405 | #[test] |
| @@ -114,7 +114,9 @@ fn main() -> eframe::Result<()> { | |||
| 114 | 114 | // rebuilt on device loss (see AudioFilesApp::poll_audio_device). | |
| 115 | 115 | let initial_stream = match audio::start_output_stream(shared.clone()) { | |
| 116 | 116 | Ok((stream, device_rate, device_name)) => { | |
| 117 | - | shared.device_sample_rate.store(device_rate, std::sync::atomic::Ordering::Relaxed); | |
| 117 | + | shared | |
| 118 | + | .device_sample_rate | |
| 119 | + | .store(device_rate, std::sync::atomic::Ordering::Relaxed); | |
| 118 | 120 | *shared.preview_device_name.lock() = Some(device_name); | |
| 119 | 121 | Some(stream) | |
| 120 | 122 | } | |
| @@ -155,7 +157,13 @@ fn main() -> eframe::Result<()> { | |||
| 155 | 157 | Box::new(move |cc| { | |
| 156 | 158 | audiofiles_browser::ui::theme::setup_fonts(&cc.egui_ctx); | |
| 157 | 159 | Ok(Box::new(AudioFilesApp::new( | |
| 158 | - | config_dir, shared, app_tray, update_checker, prefs, runtime, gtk_ok, | |
| 160 | + | config_dir, | |
| 161 | + | shared, | |
| 162 | + | app_tray, | |
| 163 | + | update_checker, | |
| 164 | + | prefs, | |
| 165 | + | runtime, | |
| 166 | + | gtk_ok, | |
| 159 | 167 | initial_stream, | |
| 160 | 168 | ))) | |
| 161 | 169 | }), | |
| @@ -165,13 +173,10 @@ fn main() -> eframe::Result<()> { | |||
| 165 | 173 | // ── API key persistence ── | |
| 166 | 174 | ||
| 167 | 175 | /// Create a SyncManager from a saved or env-provided API key. | |
| 168 | - | fn create_sync_manager( | |
| 169 | - | data_dir: &Path, | |
| 170 | - | runtime: &tokio::runtime::Handle, | |
| 171 | - | ) -> Option<SyncManager> { | |
| 176 | + | fn create_sync_manager(data_dir: &Path, runtime: &tokio::runtime::Handle) -> Option<SyncManager> { | |
| 172 | 177 | let api_key = api_key::load_api_key(data_dir)?; | |
| 173 | - | let server_url = std::env::var("AF_SYNC_SERVER_URL") | |
| 174 | - | .unwrap_or_else(|_| SYNC_SERVER_URL.to_string()); | |
| 178 | + | let server_url = | |
| 179 | + | std::env::var("AF_SYNC_SERVER_URL").unwrap_or_else(|_| SYNC_SERVER_URL.to_string()); | |
| 175 | 180 | let config = SyncKitConfig { | |
| 176 | 181 | server_url, | |
| 177 | 182 | api_key, | |
| @@ -208,7 +213,8 @@ fn resolve_initial_screen( | |||
| 208 | 213 | license_status: &license::LicenseStatus, | |
| 209 | 214 | has_trial: bool, | |
| 210 | 215 | ) -> AppScreen { | |
| 211 | - | let licensed_or_trial = matches!(license_status, license::LicenseStatus::Licensed(_)) || has_trial; | |
| 216 | + | let licensed_or_trial = | |
| 217 | + | matches!(license_status, license::LicenseStatus::Licensed(_)) || has_trial; | |
| 212 | 218 | match (vault_registry, licensed_or_trial) { | |
| 213 | 219 | (Some(_), true) => AppScreen::Browser, | |
| 214 | 220 | (Some(_), false) => AppScreen::Activation, | |
| @@ -293,10 +299,13 @@ impl AudioFilesApp { | |||
| 293 | 299 | } | |
| 294 | 300 | }; | |
| 295 | 301 | ||
| 296 | - | let has_active_trial = trial_state.as_ref().is_some_and(|t| license::trial_days_remaining(t) > 0); | |
| 302 | + | let has_active_trial = trial_state | |
| 303 | + | .as_ref() | |
| 304 | + | .is_some_and(|t| license::trial_days_remaining(t) > 0); | |
| 297 | 305 | let screen = resolve_initial_screen(&vault_registry, &license_status, has_active_trial); | |
| 298 | 306 | ||
| 299 | - | let licensed_or_trial = matches!(&license_status, license::LicenseStatus::Licensed(_)) || has_active_trial; | |
| 307 | + | let licensed_or_trial = | |
| 308 | + | matches!(&license_status, license::LicenseStatus::Licensed(_)) || has_active_trial; | |
| 300 | 309 | ||
| 301 | 310 | let (data_dir, browser, error, sync_manager, license_cache) = | |
| 302 | 311 | match (&vault_registry, &license_status) { | |
| @@ -305,7 +314,11 @@ impl AudioFilesApp { | |||
| 305 | 314 | let data_dir = reg.active.clone(); | |
| 306 | 315 | let _ = std::fs::create_dir_all(&data_dir); | |
| 307 | 316 | let sync_manager = create_sync_manager(&data_dir, runtime.handle()); | |
| 308 | - | let (browser, error) = init_browser(&data_dir, shared.clone(), &vault_setup::vault_name_for_path(reg, &data_dir)); | |
| 317 | + | let (browser, error) = init_browser( | |
| 318 | + | &data_dir, | |
| 319 | + | shared.clone(), | |
| 320 | + | &vault_setup::vault_name_for_path(reg, &data_dir), | |
| 321 | + | ); | |
| 309 | 322 | (data_dir, browser, error, sync_manager, Some(cache.clone())) | |
| 310 | 323 | } | |
| 311 | 324 | // Registry exists, unlicensed but in trial → open the active vault | |
| @@ -313,7 +326,11 @@ impl AudioFilesApp { | |||
| 313 | 326 | let data_dir = reg.active.clone(); | |
| 314 | 327 | let _ = std::fs::create_dir_all(&data_dir); | |
| 315 | 328 | let sync_manager = create_sync_manager(&data_dir, runtime.handle()); | |
| 316 | - | let (browser, error) = init_browser(&data_dir, shared.clone(), &vault_setup::vault_name_for_path(reg, &data_dir)); | |
| 329 | + | let (browser, error) = init_browser( | |
| 330 | + | &data_dir, | |
| 331 | + | shared.clone(), | |
| 332 | + | &vault_setup::vault_name_for_path(reg, &data_dir), | |
| 333 | + | ); | |
| 317 | 334 | (data_dir, browser, error, sync_manager, None) | |
| 318 | 335 | } | |
| 319 | 336 | // Registry exists but unlicensed (deactivated and reactivated) | |
| @@ -394,7 +411,9 @@ impl AudioFilesApp { | |||
| 394 | 411 | self.audio_stream = None; | |
| 395 | 412 | match audio::start_output_stream(self.shared.clone()) { | |
| 396 | 413 | Ok((stream, rate, name)) => { | |
| 397 | - | self.shared.device_sample_rate.store(rate, Ordering::Relaxed); | |
| 414 | + | self.shared | |
| 415 | + | .device_sample_rate | |
| 416 | + | .store(rate, Ordering::Relaxed); | |
| 398 | 417 | *self.shared.preview_device_name.lock() = Some(name); | |
| 399 | 418 | self.shared.device_lost.store(false, Ordering::Relaxed); | |
| 400 | 419 | self.audio_stream = Some(stream); | |
| @@ -412,7 +431,9 @@ impl AudioFilesApp { | |||
| 412 | 431 | fn activate_browser(&mut self) { | |
| 413 | 432 | let _ = std::fs::create_dir_all(&self.data_dir); | |
| 414 | 433 | self.sync_manager = create_sync_manager(&self.data_dir, self._runtime.handle()); | |
| 415 | - | let vault_name = self.vault_registry.as_ref() | |
| 434 | + | let vault_name = self | |
| 435 | + | .vault_registry | |
| 436 | + | .as_ref() | |
| 416 | 437 | .map(|r| vault_setup::vault_name_for_path(r, &self.data_dir)) | |
| 417 | 438 | .unwrap_or_else(|| "Library".to_string()); | |
| 418 | 439 | let (browser, error) = init_browser(&self.data_dir, self.shared.clone(), &vault_name); | |
| @@ -450,8 +471,14 @@ impl AudioFilesApp { | |||
| 450 | 471 | ||
| 451 | 472 | /// Create a BrowserState, returning (Some(browser), None) on success or | |
| 452 | 473 | /// (None, Some(error)) on failure. | |
| 453 | - | fn init_browser(data_dir: &Path, shared: Arc<SharedState>, vault_name: &str) -> (Option<BrowserState>, Option<String>) { | |
| 454 | - | let sample_rate = shared.device_sample_rate.load(std::sync::atomic::Ordering::Relaxed) as f32; | |
| 474 | + | fn init_browser( | |
| 475 | + | data_dir: &Path, | |
| 476 | + | shared: Arc<SharedState>, | |
| 477 | + | vault_name: &str, | |
| 478 | + | ) -> (Option<BrowserState>, Option<String>) { | |
| 479 | + | let sample_rate = shared | |
| 480 | + | .device_sample_rate | |
| 481 | + | .load(std::sync::atomic::Ordering::Relaxed) as f32; | |
| 455 | 482 | match BrowserState::new(data_dir, shared, sample_rate, vault_name) { | |
| 456 | 483 | Ok(mut browser) => { | |
| 457 | 484 | // CLI args / OS "Open with": route through the background import | |
| @@ -592,114 +619,121 @@ impl AudioFilesApp { | |||
| 592 | 619 | let ctx = &ctx; | |
| 593 | 620 | // Poll tray menu events | |
| 594 | 621 | if let Some(ref tray) = self.tray | |
| 595 | - | && let Some(action) = tray.poll() { | |
| 596 | - | match action { | |
| 597 | - | tray::TrayAction::ShowWindow => { | |
| 598 | - | ctx.send_viewport_cmd(ViewportCommand::Focus); | |
| 599 | - | } | |
| 600 | - | tray::TrayAction::TogglePlayback => { | |
| 601 | - | if let Some(ref mut browser) = self.browser { | |
| 602 | - | browser.toggle_preview(); | |
| 603 | - | } | |
| 604 | - | } | |
| 605 | - | tray::TrayAction::Quit => { | |
| 606 | - | ctx.send_viewport_cmd(ViewportCommand::Close); | |
| 622 | + | && let Some(action) = tray.poll() | |
| 623 | + | { | |
| 624 | + | match action { | |
| 625 | + | tray::TrayAction::ShowWindow => { | |
| 626 | + | ctx.send_viewport_cmd(ViewportCommand::Focus); | |
| 627 | + | } | |
| 628 | + | tray::TrayAction::TogglePlayback => { | |
| 629 | + | if let Some(ref mut browser) = self.browser { | |
| 630 | + | browser.toggle_preview(); | |
| 607 | 631 | } | |
| 608 | 632 | } | |
| 633 | + | tray::TrayAction::Quit => { | |
| 634 | + | ctx.send_viewport_cmd(ViewportCommand::Close); | |
| 635 | + | } | |
| 609 | 636 | } | |
| 637 | + | } | |
| 610 | 638 | ||
| 611 | 639 | // Update tray tooltip based on playback state | |
| 612 | 640 | if let Some(ref tray) = self.tray | |
| 613 | - | && let Some(ref browser) = self.browser { | |
| 614 | - | let playing = browser.shared.preview.lock().playing; | |
| 615 | - | if playing { | |
| 616 | - | tray.set_tooltip(&browser.status); | |
| 617 | - | } else { | |
| 618 | - | tray.set_tooltip("audiofiles"); | |
| 619 | - | } | |
| 641 | + | && let Some(ref browser) = self.browser | |
| 642 | + | { | |
| 643 | + | let playing = browser.shared.preview.lock().playing; | |
| 644 | + | if playing { | |
| 645 | + | tray.set_tooltip(&browser.status); | |
| 646 | + | } else { | |
| 647 | + | tray.set_tooltip("audiofiles"); | |
| 620 | 648 | } | |
| 649 | + | } | |
| 621 | 650 | ||
| 622 | 651 | // Check if sync pulled remote changes → refresh browser contents | |
| 623 | 652 | if let Some(ref sync) = self.sync_manager | |
| 624 | - | && sync.status().needs_refresh { | |
| 625 | - | if let Some(ref mut browser) = self.browser { | |
| 626 | - | browser.refresh_vfs_list(); | |
| 627 | - | browser.refresh_contents(); | |
| 628 | - | } | |
| 629 | - | sync.clear_needs_refresh(); | |
| 653 | + | && sync.status().needs_refresh | |
| 654 | + | { | |
| 655 | + | if let Some(ref mut browser) = self.browser { | |
| 656 | + | browser.refresh_vfs_list(); | |
| 657 | + | browser.refresh_contents(); | |
| 630 | 658 | } | |
| 659 | + | sync.clear_needs_refresh(); | |
| 660 | + | } | |
| 631 | 661 | ||
| 632 | 662 | // ── Sync setup actions (before draw, so UI sees results this frame) ── | |
| 633 | 663 | // ── Vault actions ── | |
| 634 | 664 | if let Some(ref mut browser) = self.browser | |
| 635 | - | && let Some(action) = browser.settings.pending_action.take() { | |
| 636 | - | use audiofiles_browser::state::VaultAction; | |
| 637 | - | match action { | |
| 638 | - | VaultAction::SwitchVault(path) => { | |
| 639 | - | self.switch_vault(path); | |
| 640 | - | return; | |
| 641 | - | } | |
| 642 | - | VaultAction::CreateVault { name, path, loose_files } => { | |
| 643 | - | let switch_path = path.clone(); | |
| 644 | - | if self.with_vault_registry(|reg| vault::create_vault(reg, &name, &path)) { | |
| 645 | - | self.switch_vault(switch_path); | |
| 646 | - | if loose_files | |
| 647 | - | && let Some(ref mut browser) = self.browser { | |
| 648 | - | let _ = browser.backend.set_config("loose_files", "1"); | |
| 649 | - | browser.settings.is_loose_files = true; | |
| 650 | - | } | |
| 651 | - | return; | |
| 665 | + | && let Some(action) = browser.settings.pending_action.take() | |
| 666 | + | { | |
| 667 | + | use audiofiles_browser::state::VaultAction; | |
| 668 | + | match action { | |
| 669 | + | VaultAction::SwitchVault(path) => { | |
| 670 | + | self.switch_vault(path); | |
| 671 | + | return; | |
| 672 | + | } | |
| 673 | + | VaultAction::CreateVault { | |
| 674 | + | name, | |
| 675 | + | path, | |
| 676 | + | loose_files, | |
| 677 | + | } => { | |
| 678 | + | let switch_path = path.clone(); | |
| 679 | + | if self.with_vault_registry(|reg| vault::create_vault(reg, &name, &path)) { | |
| 680 | + | self.switch_vault(switch_path); | |
| 681 | + | if loose_files && let Some(ref mut browser) = self.browser { | |
| 682 | + | let _ = browser.backend.set_config("loose_files", "1"); | |
| 683 | + | browser.settings.is_loose_files = true; | |
| 652 | 684 | } | |
| 685 | + | return; | |
| 653 | 686 | } | |
| 654 | - | VaultAction::AddExistingVault { name, path } => { | |
| 655 | - | self.with_vault_registry(|reg| vault::add_existing_vault(reg, &name, &path)); | |
| 656 | - | } | |
| 657 | - | VaultAction::RemoveVault(path) => { | |
| 658 | - | self.with_vault_registry(|reg| vault::remove_vault(reg, &path)); | |
| 659 | - | } | |
| 660 | - | VaultAction::RenameVault { path, new_name } => { | |
| 661 | - | self.with_vault_registry(|reg| vault::rename_vault(reg, &path, &new_name)); | |
| 662 | - | } | |
| 663 | - | VaultAction::RelocateVault { old_path, new_path } => { | |
| 664 | - | // If we're repointing the active vault, switch to the new | |
| 665 | - | // path so the open browser picks up the new location. | |
| 666 | - | let was_active = self | |
| 667 | - | .vault_registry | |
| 668 | - | .as_ref() | |
| 669 | - | .map(|r| r.active == old_path) | |
| 670 | - | .unwrap_or(false); | |
| 671 | - | let ok = self.with_vault_registry(|reg| { | |
| 672 | - | vault::relocate_vault(reg, &old_path, &new_path) | |
| 673 | - | }); | |
| 674 | - | if ok && was_active { | |
| 675 | - | self.switch_vault(new_path); | |
| 676 | - | return; | |
| 677 | - | } | |
| 687 | + | } | |
| 688 | + | VaultAction::AddExistingVault { name, path } => { | |
| 689 | + | self.with_vault_registry(|reg| vault::add_existing_vault(reg, &name, &path)); | |
| 690 | + | } | |
| 691 | + | VaultAction::RemoveVault(path) => { | |
| 692 | + | self.with_vault_registry(|reg| vault::remove_vault(reg, &path)); | |
| 693 | + | } | |
| 694 | + | VaultAction::RenameVault { path, new_name } => { | |
| 695 | + | self.with_vault_registry(|reg| vault::rename_vault(reg, &path, &new_name)); | |
| 696 | + | } | |
| 697 | + | VaultAction::RelocateVault { old_path, new_path } => { | |
| 698 | + | // If we're repointing the active vault, switch to the new | |
| 699 | + | // path so the open browser picks up the new location. | |
| 700 | + | let was_active = self | |
| 701 | + | .vault_registry | |
| 702 | + | .as_ref() | |
| 703 | + | .map(|r| r.active == old_path) | |
| 704 | + | .unwrap_or(false); | |
| 705 | + | let ok = self.with_vault_registry(|reg| { | |
| 706 | + | vault::relocate_vault(reg, &old_path, &new_path) | |
| 707 | + | }); | |
| 708 | + | if ok && was_active { | |
| 709 | + | self.switch_vault(new_path); | |
| 710 | + | return; | |
| 678 | 711 | } | |
| 679 | - | VaultAction::ScanStorage => { | |
| 680 | - | // storage_stats() is a single indexed COUNT/SUM — fast | |
| 681 | - | // enough to run inline; no in-progress flag needed (the | |
| 682 | - | // old one was set and cleared in the same frame, so its | |
| 683 | - | // spinner could never render). | |
| 684 | - | match browser.backend.storage_stats() { | |
| 685 | - | Ok(stats) => { | |
| 686 | - | browser.settings.storage_cache = Some(stats); | |
| 687 | - | browser.settings.storage_cache_at = Some( | |
| 688 | - | std::time::SystemTime::now() | |
| 689 | - | .duration_since(std::time::UNIX_EPOCH) | |
| 690 | - | .map(|d| d.as_secs() as i64) | |
| 691 | - | .unwrap_or(0), | |
| 692 | - | ); | |
| 693 | - | } | |
| 694 | - | Err(e) => browser.status = format!("Storage scan failed: {e}"), | |
| 712 | + | } | |
| 713 | + | VaultAction::ScanStorage => { | |
| 714 | + | // storage_stats() is a single indexed COUNT/SUM — fast | |
| 715 | + | // enough to run inline; no in-progress flag needed (the | |
| 716 | + | // old one was set and cleared in the same frame, so its | |
| 717 | + | // spinner could never render). | |
| 718 | + | match browser.backend.storage_stats() { | |
| 719 | + | Ok(stats) => { | |
| 720 | + | browser.settings.storage_cache = Some(stats); | |
| 721 | + | browser.settings.storage_cache_at = Some( | |
| 722 | + | std::time::SystemTime::now() | |
| 723 | + | .duration_since(std::time::UNIX_EPOCH) | |
| 724 | + | .map(|d| d.as_secs() as i64) | |
| 725 | + | .unwrap_or(0), | |
| 726 | + | ); | |
| 695 | 727 | } | |
| 728 | + | Err(e) => browser.status = format!("Storage scan failed: {e}"), | |
| 696 | 729 | } | |
| 697 | - | VaultAction::DeactivateLicense => { | |
| 698 | - | self.deactivate(); | |
| 699 | - | return; | |
| 700 | - | } | |
| 730 | + | } | |
| 731 | + | VaultAction::DeactivateLicense => { | |
| 732 | + | self.deactivate(); | |
| 733 | + | return; | |
| 701 | 734 | } | |
| 702 | 735 | } | |
| 736 | + | } | |
| 703 | 737 | ||
| 704 | 738 | // ── VFS Mirror: sync if dirty ── | |
| 705 | 739 | if let Some(ref mut browser) = self.browser { | |
| @@ -715,24 +749,24 @@ impl AudioFilesApp { | |||
| 715 | 749 | MidiAction::RefreshPorts => { | |
| 716 | 750 | browser.midi_state.available_ports = midi::list_input_ports(); | |
| 717 | 751 | } | |
| 718 | - | MidiAction::Connect(idx) => { | |
| 719 | - | match midi::connect(idx, self.shared.clone()) { | |
| 720 | - | Ok(conn) => { | |
| 721 | - | let name = browser.midi_state.available_ports | |
| 722 | - | .get(idx) | |
| 723 | - | .cloned() | |
| 724 | - | .unwrap_or_else(|| format!("Port {idx}")); | |
| 725 | - | browser.midi_state.connected_port = Some(idx); | |
| 726 | - | browser.midi_state.connected_port_name = Some(name); | |
| 727 | - | self.midi_connection = Some(conn); | |
| 728 | - | } | |
| 729 | - | Err(e) => { | |
| 730 | - | tracing::error!("MIDI connect failed: {e}"); | |
| 731 | - | browser.midi_state.connected_port = None; | |
| 732 | - | browser.midi_state.connected_port_name = None; | |
| 733 | - | } | |
| 752 | + | MidiAction::Connect(idx) => match midi::connect(idx, self.shared.clone()) { | |
| 753 | + | Ok(conn) => { | |
| 754 | + | let name = browser | |
| 755 | + | .midi_state | |
| 756 | + | .available_ports | |
| 757 | + | .get(idx) | |
| 758 | + | .cloned() | |
| 759 | + | .unwrap_or_else(|| format!("Port {idx}")); | |
| 760 | + | browser.midi_state.connected_port = Some(idx); | |
| 761 | + | browser.midi_state.connected_port_name = Some(name); | |
| 762 | + | self.midi_connection = Some(conn); | |
| 734 | 763 | } | |
| 735 | - | } | |
| 764 | + | Err(e) => { | |
| 765 | + | tracing::error!("MIDI connect failed: {e}"); | |
| 766 | + | browser.midi_state.connected_port = None; | |
| 767 | + | browser.midi_state.connected_port_name = None; | |
| 768 | + | } | |
| 769 | + | }, | |
| 736 | 770 | MidiAction::Disconnect => { | |
| 737 | 771 | self.midi_connection = None; | |
| 738 | 772 | browser.midi_state.connected_port = None; | |
| @@ -937,10 +971,7 @@ impl AudioFilesApp { | |||
| 937 | 971 | ui.separator(); | |
| 938 | 972 | ui.add_space(8.0); | |
| 939 | 973 | ui.strong("Network"); | |
| 940 | - | ui.checkbox( | |
| 941 | - | &mut updated_check_pref, | |
| 942 | - | "Check makenot.work for updates", | |
| 943 | - | ); | |
| 974 | + | ui.checkbox(&mut updated_check_pref, "Check makenot.work for updates"); | |
| 944 | 975 | ui.label( | |
| 945 | 976 | egui::RichText::new( | |
| 946 | 977 | "When enabled, the app contacts makenot.work on launch and every 6 hours \ | |
| @@ -1037,13 +1068,19 @@ mod tests { | |||
| 1037 | 1068 | let dir = tempfile::tempdir().unwrap(); | |
| 1038 | 1069 | let reg = Some(make_registry(dir.path())); | |
| 1039 | 1070 | let status = license::LicenseStatus::Licensed(make_license_cache()); | |
| 1040 | - | assert_eq!(resolve_initial_screen(®, &status, false), AppScreen::Browser); | |
| 1071 | + | assert_eq!( | |
| 1072 | + | resolve_initial_screen(®, &status, false), | |
| 1073 | + | AppScreen::Browser | |
| 1074 | + | ); | |
| 1041 | 1075 | } | |
| 1042 | 1076 | ||
| 1043 | 1077 | #[test] | |
| 1044 | 1078 | fn initial_screen_licensed_without_registry() { | |
| 1045 | 1079 | let status = license::LicenseStatus::Licensed(make_license_cache()); | |
| 1046 | - | assert_eq!(resolve_initial_screen(&None, &status, false), AppScreen::VaultSetup); | |
| 1080 | + | assert_eq!( | |
| 1081 | + | resolve_initial_screen(&None, &status, false), | |
| 1082 | + | AppScreen::VaultSetup | |
| 1083 | + | ); | |
| 1047 | 1084 | } | |
| 1048 | 1085 | ||
| 1049 | 1086 | #[test] | |
| @@ -1051,13 +1088,19 @@ mod tests { | |||
| 1051 | 1088 | let dir = tempfile::tempdir().unwrap(); | |
| 1052 | 1089 | let reg = Some(make_registry(dir.path())); | |
| 1053 | 1090 | let status = license::LicenseStatus::Unlicensed; | |
| 1054 | - | assert_eq!(resolve_initial_screen(®, &status, false), AppScreen::Activation); | |
| 1091 | + | assert_eq!( | |
| 1092 | + | resolve_initial_screen(®, &status, false), | |
| 1093 | + | AppScreen::Activation | |
| 1094 | + | ); | |
| 1055 | 1095 | } | |
| 1056 | 1096 | ||
| 1057 | 1097 | #[test] | |
| 1058 | 1098 | fn initial_screen_unlicensed_without_registry() { | |
| 1059 | 1099 | let status = license::LicenseStatus::Unlicensed; | |
| 1060 | - | assert_eq!(resolve_initial_screen(&None, &status, false), AppScreen::Activation); | |
| 1100 | + | assert_eq!( | |
| 1101 | + | resolve_initial_screen(&None, &status, false), | |
| 1102 | + | AppScreen::Activation | |
| 1103 | + | ); | |
| 1061 | 1104 | } | |
| 1062 | 1105 | ||
| 1063 | 1106 | #[test] | |
| @@ -1065,13 +1108,19 @@ mod tests { | |||
| 1065 | 1108 | let dir = tempfile::tempdir().unwrap(); | |
| 1066 | 1109 | let reg = Some(make_registry(dir.path())); | |
| 1067 | 1110 | let status = license::LicenseStatus::Unlicensed; | |
| 1068 | - | assert_eq!(resolve_initial_screen(®, &status, true), AppScreen::Browser); | |
| 1111 | + | assert_eq!( | |
| 1112 | + | resolve_initial_screen(®, &status, true), | |
| 1113 | + | AppScreen::Browser | |
| 1114 | + | ); | |
| 1069 | 1115 | } | |
| 1070 | 1116 | ||
| 1071 | 1117 | #[test] | |
| 1072 | 1118 | fn initial_screen_trial_without_registry() { | |
| 1073 | 1119 | let status = license::LicenseStatus::Unlicensed; | |
| 1074 | - | assert_eq!(resolve_initial_screen(&None, &status, true), AppScreen::VaultSetup); | |
| 1120 | + | assert_eq!( | |
| 1121 | + | resolve_initial_screen(&None, &status, true), | |
| 1122 | + | AppScreen::VaultSetup | |
| 1123 | + | ); | |
| 1075 | 1124 | } | |
| 1076 | 1125 | ||
| 1077 | 1126 | // ── License migration ── | |
| @@ -1080,7 +1129,11 @@ mod tests { | |||
| 1080 | 1129 | fn migrate_license_copies_files() { | |
| 1081 | 1130 | let src = tempfile::tempdir().unwrap(); | |
| 1082 | 1131 | let dst = tempfile::tempdir().unwrap(); | |
| 1083 | - | std::fs::write(src.path().join("license.json"), r#"{"key_code":"k","machine_id":"m","activated_at":"t"}"#).unwrap(); | |
| 1132 | + | std::fs::write( | |
| 1133 | + | src.path().join("license.json"), | |
| 1134 | + | r#"{"key_code":"k","machine_id":"m","activated_at":"t"}"#, | |
| 1135 | + | ) | |
| 1136 | + | .unwrap(); | |
| 1084 | 1137 | std::fs::write(src.path().join("machine_id"), "mid-123").unwrap(); | |
| 1085 | 1138 | ||
| 1086 | 1139 | vault_setup::migrate_license_to_config(dst.path(), src.path()); | |
| @@ -1154,13 +1207,19 @@ mod tests { | |||
| 1154 | 1207 | fn vault_name_for_path_found() { | |
| 1155 | 1208 | let dir = tempfile::tempdir().unwrap(); | |
| 1156 | 1209 | let reg = make_registry(dir.path()); | |
| 1157 | - | assert_eq!(vault_setup::vault_name_for_path(®, dir.path()), "Library"); | |
| 1210 | + | assert_eq!( | |
| 1211 | + | vault_setup::vault_name_for_path(®, dir.path()), | |
| 1212 | + | "Library" | |
| 1213 | + | ); | |
| 1158 | 1214 | } | |
| 1159 | 1215 | ||
| 1160 | 1216 | #[test] | |
| 1161 | 1217 | fn vault_name_for_path_not_found() { | |
| 1162 | 1218 | let dir = tempfile::tempdir().unwrap(); | |
| 1163 | 1219 | let reg = make_registry(dir.path()); | |
| 1164 | - | assert_eq!(vault_setup::vault_name_for_path(®, Path::new("/nonexistent")), "Library"); | |
| 1220 | + | assert_eq!( | |
| 1221 | + | vault_setup::vault_name_for_path(®, Path::new("/nonexistent")), | |
| 1222 | + | "Library" | |
| 1223 | + | ); | |
| 1165 | 1224 | } | |
| 1166 | 1225 | } |
| @@ -91,35 +91,33 @@ pub fn list_input_ports() -> Vec<String> { | |||
| 91 | 91 | pub fn connect(port_index: usize, shared: Arc<SharedState>) -> Result<MidiConnection, MidiError> { | |
| 92 | 92 | let midi_in = MidiInput::new("audiofiles-input")?; | |
| 93 | 93 | let ports = midi_in.ports(); | |
| 94 | - | let port = ports | |
| 95 | - | .get(port_index) | |
| 96 | - | .ok_or(MidiError::PortOutOfRange { idx: port_index, count: ports.len() })?; | |
| 94 | + | let port = ports.get(port_index).ok_or(MidiError::PortOutOfRange { | |
| 95 | + | idx: port_index, | |
| 96 | + | count: ports.len(), | |
| 97 | + | })?; | |
| 97 | 98 | ||
| 98 | 99 | let shared_cb = shared.clone(); | |
| 99 | - | let conn = midi_in | |
| 100 | - | .connect( | |
| 101 | - | port, | |
| 102 | - | "audiofiles-midi-in", | |
| 103 | - | move |_timestamp, message, _| { | |
| 104 | - | match parse_note_message(message) { | |
| 105 | - | NoteAction::On { note, velocity } => { | |
| 106 | - | shared_cb.instrument.lock().note_on(note, velocity); | |
| 107 | - | let event = MidiNoteEvent { | |
| 108 | - | note, | |
| 109 | - | velocity, | |
| 110 | - | note_name: note_name(note), | |
| 111 | - | timestamp: Instant::now(), | |
| 112 | - | }; | |
| 113 | - | push_note_event_capped(&mut shared_cb.midi_recent_notes.lock(), event); | |
| 114 | - | } | |
| 115 | - | NoteAction::Off { note } => { | |
| 116 | - | shared_cb.instrument.lock().note_off(note); | |
| 117 | - | } | |
| 118 | - | NoteAction::Ignore => {} | |
| 119 | - | } | |
| 120 | - | }, | |
| 121 | - | (), | |
| 122 | - | )?; | |
| 100 | + | let conn = midi_in.connect( | |
| 101 | + | port, | |
| 102 | + | "audiofiles-midi-in", | |
| 103 | + | move |_timestamp, message, _| match parse_note_message(message) { | |
| 104 | + | NoteAction::On { note, velocity } => { | |
| 105 | + | shared_cb.instrument.lock().note_on(note, velocity); | |
| 106 | + | let event = MidiNoteEvent { | |
| 107 | + | note, | |
| 108 | + | velocity, | |
| 109 | + | note_name: note_name(note), | |
| 110 | + | timestamp: Instant::now(), | |
| 111 | + | }; | |
| 112 | + | push_note_event_capped(&mut shared_cb.midi_recent_notes.lock(), event); | |
| 113 | + | } | |
| 114 | + | NoteAction::Off { note } => { | |
| 115 | + | shared_cb.instrument.lock().note_off(note); | |
| 116 | + | } | |
| 117 | + | NoteAction::Ignore => {} | |
| 118 | + | }, | |
| 119 | + | (), | |
| 120 | + | )?; | |
| 123 | 121 | ||
| 124 | 122 | Ok(MidiConnection { _conn: conn }) | |
| 125 | 123 | } | |
| @@ -143,7 +141,10 @@ mod tests { | |||
| 143 | 141 | fn parse_note_on() { | |
| 144 | 142 | assert_eq!( | |
| 145 | 143 | parse_note_message(&[0x90, 60, 100]), | |
| 146 | - | NoteAction::On { note: 60, velocity: 100 } | |
| 144 | + | NoteAction::On { | |
| 145 | + | note: 60, | |
| 146 | + | velocity: 100 | |
| 147 | + | } | |
| 147 | 148 | ); | |
| 148 | 149 | } | |
| 149 | 150 | ||
| @@ -169,7 +170,10 @@ mod tests { | |||
| 169 | 170 | // Low nibble is the channel; note-on on channel 15 (0x9F) still parses. | |
| 170 | 171 | assert_eq!( | |
| 171 | 172 | parse_note_message(&[0x9F, 72, 1]), | |
| 172 | - | NoteAction::On { note: 72, velocity: 1 } | |
| 173 | + | NoteAction::On { | |
| 174 | + | note: 72, | |
| 175 | + | velocity: 1 | |
| 176 | + | } | |
| 173 | 177 | ); | |
| 174 | 178 | // Note-off on channel 7 (0x87). | |
| 175 | 179 | assert_eq!( | |
| @@ -199,7 +203,10 @@ mod tests { | |||
| 199 | 203 | // A message longer than 3 bytes still parses off the first three. | |
| 200 | 204 | assert_eq!( | |
| 201 | 205 | parse_note_message(&[0x90, 64, 80, 0xFF, 0xFF]), | |
| 202 | - | NoteAction::On { note: 64, velocity: 80 } | |
| 206 | + | NoteAction::On { | |
| 207 | + | note: 64, | |
| 208 | + | velocity: 80 | |
| 209 | + | } | |
| 203 | 210 | ); | |
| 204 | 211 | } | |
| 205 | 212 | ||
| @@ -208,11 +215,17 @@ mod tests { | |||
| 208 | 215 | // Full note range 0..=127. | |
| 209 | 216 | assert_eq!( | |
| 210 | 217 | parse_note_message(&[0x90, 0, 1]), | |
| 211 | - | NoteAction::On { note: 0, velocity: 1 } | |
| 218 | + | NoteAction::On { | |
| 219 | + | note: 0, | |
| 220 | + | velocity: 1 | |
| 221 | + | } | |
| 212 | 222 | ); | |
| 213 | 223 | assert_eq!( | |
| 214 | 224 | parse_note_message(&[0x90, 127, 127]), | |
| 215 | - | NoteAction::On { note: 127, velocity: 127 } | |
| 225 | + | NoteAction::On { | |
| 226 | + | note: 127, | |
| 227 | + | velocity: 127 | |
| 228 | + | } | |
| 216 | 229 | ); | |
| 217 | 230 | } | |
| 218 | 231 |
| @@ -80,7 +80,9 @@ mod tests { | |||
| 80 | 80 | #[test] | |
| 81 | 81 | fn save_then_load_roundtrip() { | |
| 82 | 82 | let dir = tempfile::tempdir().unwrap(); | |
| 83 | - | let p = Preferences { check_for_updates: false }; | |
| 83 | + | let p = Preferences { | |
| 84 | + | check_for_updates: false, | |
| 85 | + | }; | |
| 84 | 86 | p.save(dir.path()); | |
| 85 | 87 | let loaded = Preferences::load(dir.path()); | |
| 86 | 88 | assert!(!loaded.check_for_updates); |
| @@ -182,11 +182,10 @@ impl UpdateChecker { | |||
| 182 | 182 | ||
| 183 | 183 | /// Verify that a download URL points to a trusted domain. | |
| 184 | 184 | pub fn is_trusted_download_url(url: &str) -> bool { | |
| 185 | - | const TRUSTED_PREFIXES: &[&str] = &[ | |
| 186 | - | "https://makenot.work/", | |
| 187 | - | "https://dist.makenot.work/", | |
| 188 | - | ]; | |
| 189 | - | TRUSTED_PREFIXES.iter().any(|prefix| url.starts_with(prefix)) | |
| 185 | + | const TRUSTED_PREFIXES: &[&str] = &["https://makenot.work/", "https://dist.makenot.work/"]; | |
| 186 | + | TRUSTED_PREFIXES | |
| 187 | + | .iter() | |
| 188 | + | .any(|prefix| url.starts_with(prefix)) | |
| 190 | 189 | } | |
| 191 | 190 | ||
| 192 | 191 | /// Check the MNW OTA endpoint once. | |
| @@ -239,15 +238,17 @@ async fn check_once(status: &Arc<Mutex<UpdateStatus>>) { | |||
| 239 | 238 | Ok(body) => match serde_json::from_slice::<UpdateResponse>(&body) { | |
| 240 | 239 | Ok(mut update) => { | |
| 241 | 240 | if let Ok(remote) = Version::parse(&update.version) | |
| 242 | - | && remote > current && is_trusted_download_url(&update.url) { | |
| 243 | - | tracing::info!("Update available: v{}", update.version); | |
| 244 | - | truncate_chars(&mut update.notes, MAX_NOTES_LEN); | |
| 245 | - | let mut s = status.lock(); | |
| 246 | - | s.available = true; | |
| 247 | - | s.version = update.version; | |
| 248 | - | s.notes = update.notes; | |
| 249 | - | s.download_url = update.url; | |
| 250 | - | } | |
| 241 | + | && remote > current | |
| 242 | + | && is_trusted_download_url(&update.url) | |
| 243 | + | { | |
| 244 | + | tracing::info!("Update available: v{}", update.version); | |
| 245 | + | truncate_chars(&mut update.notes, MAX_NOTES_LEN); | |
| 246 | + | let mut s = status.lock(); | |
| 247 | + | s.available = true; | |
| 248 | + | s.version = update.version; | |
| 249 | + | s.notes = update.notes; | |
| 250 | + | s.download_url = update.url; | |
| 251 | + | } | |
| 251 | 252 | } | |
| 252 | 253 | Err(e) => { | |
| 253 | 254 | tracing::warn!("Failed to parse update response: {e}"); | |
| @@ -429,7 +430,6 @@ mod tests { | |||
| 429 | 430 | ||
| 430 | 431 | #[test] | |
| 431 | 432 | fn current_version_is_valid_semver() { | |
| 432 | - | Version::parse(CURRENT_VERSION) | |
| 433 | - | .expect("CURRENT_VERSION should be valid semver"); | |
| 433 | + | Version::parse(CURRENT_VERSION).expect("CURRENT_VERSION should be valid semver"); | |
| 434 | 434 | } | |
| 435 | 435 | } |
| @@ -75,9 +75,10 @@ impl AudioFilesApp { | |||
| 75 | 75 | ui.add_space(theme::space::SM); | |
| 76 | 76 | ui.horizontal(|ui| { | |
| 77 | 77 | if ui.button("Choose different location...").clicked() | |
| 78 | - | && let Some(path) = rfd::FileDialog::new().pick_folder() { | |
| 79 | - | self.vault_setup_path = Some(path); | |
| 80 | - | } | |
| 78 | + | && let Some(path) = rfd::FileDialog::new().pick_folder() | |
| 79 | + | { | |
| 80 | + | self.vault_setup_path = Some(path); | |
| 81 | + | } | |
| 81 | 82 | if is_custom && ui.button("Use default").clicked() { | |
| 82 | 83 | self.vault_setup_path = None; | |
| 83 | 84 | } | |
| @@ -164,7 +165,9 @@ impl AudioFilesApp { | |||
| 164 | 165 | where | |
| 165 | 166 | F: FnOnce(&mut VaultRegistry) -> Result<(), vault::VaultError>, | |
| 166 | 167 | { | |
| 167 | - | let Some(ref mut reg) = self.vault_registry else { return false }; | |
| 168 | + | let Some(ref mut reg) = self.vault_registry else { | |
| 169 | + | return false; | |
| 170 | + | }; | |
| 168 | 171 | if let Err(e) = op(reg) { | |
| 169 | 172 | if let Some(ref mut b) = self.browser { | |
| 170 | 173 | b.status = format!("Vault error: {e}"); | |
| @@ -185,7 +188,13 @@ impl AudioFilesApp { | |||
| 185 | 188 | browser.settings.list = reg | |
| 186 | 189 | .vaults | |
| 187 | 190 | .iter() | |
| 188 | - | .map(|v| (v.name.clone(), v.path.clone(), vault::is_vault_reachable(&v.path))) | |
| 191 | + | .map(|v| { | |
| 192 | + | ( | |
| 193 | + | v.name.clone(), | |
| 194 | + | v.path.clone(), | |
| 195 | + | vault::is_vault_reachable(&v.path), | |
| 196 | + | ) | |
| 197 | + | }) | |
| 189 | 198 | .collect(); | |
| 190 | 199 | browser.settings.name = vault_name_for_path(reg, &self.data_dir); | |
| 191 | 200 | } | |
| @@ -207,7 +216,11 @@ pub(crate) fn migrate_license_to_config(config_dir: &Path, default_vault: &Path) | |||
| 207 | 216 | if let Err(e) = std::fs::copy(&src, &dst) { | |
| 208 | 217 | tracing::warn!("Failed to migrate {filename} to config_dir: {e}"); | |
| 209 | 218 | } else { | |
| 210 | - | tracing::info!("Migrated {filename} from {} to {}", src.display(), dst.display()); | |
| 219 | + | tracing::info!( | |
| 220 | + | "Migrated {filename} from {} to {}", | |
| 221 | + | src.display(), | |
| 222 | + | dst.display() | |
| 223 | + | ); | |
| 211 | 224 | } | |
| 212 | 225 | } | |
| 213 | 226 | } |
| @@ -55,8 +55,7 @@ fn time_stages(path: &Path) -> Option<(StageTiming, f64, u32)> { | |||
| 55 | 55 | ||
| 56 | 56 | // Spectral | |
| 57 | 57 | let t = Instant::now(); | |
| 58 | - | let (features, magnitude_frames) = | |
| 59 | - | spectral::compute_spectral_features_with_frames(capped, sr); | |
| 58 | + | let (features, magnitude_frames) = spectral::compute_spectral_features_with_frames(capped, sr); | |
| 60 | 59 | let spectral_ms = t.elapsed().as_secs_f64() * 1000.0; | |
| 61 | 60 | ||
| 62 | 61 | // MFCC | |
| @@ -149,10 +148,10 @@ fn classify_file(path: &Path) -> Option<(SampleClass, f64)> { | |||
| 149 | 148 | ||
| 150 | 149 | let crest = basic::crest_factor(&decoded.samples); | |
| 151 | 150 | let attack = basic::attack_time(&decoded.samples, sr); | |
| 152 | - | let (features, magnitude_frames) = | |
| 153 | - | spectral::compute_spectral_features_with_frames(capped, sr); | |
| 151 | + | let (features, magnitude_frames) = spectral::compute_spectral_features_with_frames(capped, sr); | |
| 154 | 152 | let mfcc_features = mfcc::compute_mfccs(&magnitude_frames, sr, 1024); | |
| 155 | - | let input = ClassifyInput::with_mfccs(&features, decoded.duration, crest, attack, &mfcc_features); | |
| 153 | + | let input = | |
| 154 | + | ClassifyInput::with_mfccs(&features, decoded.duration, crest, attack, &mfcc_features); | |
| 156 | 155 | let result = classify::classify_ml(&input); | |
| 157 | 156 | Some((result.class, result.confidence)) | |
| 158 | 157 | } | |
| @@ -175,9 +174,10 @@ fn collect_audio_files(dir: &Path, limit: Option<usize>) -> Vec<PathBuf> { | |||
| 175 | 174 | } | |
| 176 | 175 | for entry in walkdir(dir) { | |
| 177 | 176 | if let Some(lim) = limit | |
| 178 | - | && files.len() >= lim { | |
| 179 | - | break; | |
| 180 | - | } | |
| 177 | + | && files.len() >= lim | |
| 178 | + | { | |
| 179 | + | break; | |
| 180 | + | } | |
| 181 | 181 | files.push(entry); | |
| 182 | 182 | } | |
| 183 | 183 | files | |
| @@ -241,14 +241,46 @@ fn main() { | |||
| 241 | 241 | // Collect samples from different sources for timing | |
| 242 | 242 | let timing_sources: Vec<(&str, PathBuf, Option<usize>)> = vec![ | |
| 243 | 243 | ("drum one-shots (WAV)", training_dir.join("kick"), Some(200)), | |
| 244 | - | ("drum one-shots (WAV)", training_dir.join("snare"), Some(200)), | |
| 245 | - | ("drum one-shots (WAV)", training_dir.join("hihat"), Some(200)), | |
| 246 | - | ("philharmonia (MP3)", test_suite_dir.join("formats/mp3"), Some(50)), | |
| 247 | - | ("AIFF samples", test_suite_dir.join("formats/aiff"), Some(10)), | |
| 248 | - | ("FLAC samples", test_suite_dir.join("formats/flac"), Some(10)), | |
| 249 | - | ("ambient loops (WAV)", test_suite_dir.join("genres/ambient"), Some(50)), | |
| 250 | - | ("synth loops (WAV)", test_suite_dir.join("genres/synth"), Some(50)), | |
| 251 | - | ("guitar loops (WAV)", test_suite_dir.join("genres/guitar"), Some(50)), | |
| 244 | + | ( | |
| 245 | + | "drum one-shots (WAV)", | |
| 246 | + | training_dir.join("snare"), | |
| 247 | + | Some(200), | |
| 248 | + | ), | |
| 249 | + | ( | |
| 250 | + | "drum one-shots (WAV)", | |
| 251 | + | training_dir.join("hihat"), | |
| 252 | + | Some(200), | |
| 253 | + | ), | |
| 254 | + | ( | |
| 255 | + | "philharmonia (MP3)", | |
| 256 | + | test_suite_dir.join("formats/mp3"), | |
| 257 | + | Some(50), | |
| 258 | + | ), | |
| 259 | + | ( | |
| 260 | + | "AIFF samples", | |
| 261 | + | test_suite_dir.join("formats/aiff"), | |
| 262 | + | Some(10), | |
| 263 | + | ), | |
| 264 | + | ( | |
| 265 | + | "FLAC samples", | |
| 266 | + | test_suite_dir.join("formats/flac"), | |
| 267 | + | Some(10), | |
| 268 | + | ), | |
| 269 | + | ( | |
| 270 | + | "ambient loops (WAV)", | |
| 271 | + | test_suite_dir.join("genres/ambient"), | |
| 272 | + | Some(50), | |
| 273 | + | ), | |
| 274 | + | ( | |
| 275 | + | "synth loops (WAV)", | |
| 276 | + | test_suite_dir.join("genres/synth"), | |
| 277 | + | Some(50), | |
| 278 | + | ), | |
| 279 | + | ( | |
| 280 | + | "guitar loops (WAV)", | |
| 281 | + | test_suite_dir.join("genres/guitar"), | |
| 282 | + | Some(50), | |
| 283 | + | ), | |
| 252 | 284 | ]; | |
| 253 | 285 | ||
| 254 | 286 | let mut all_timings: Vec<(StageTiming, f64, u32, String)> = Vec::new(); | |
| @@ -260,9 +292,7 @@ fn main() { | |||
| 260 | 292 | } | |
| 261 | 293 | let results: Vec<_> = files | |
| 262 | 294 | .par_iter() | |
| 263 | - | .filter_map(|f| { | |
| 264 | - | time_stages(f).map(|(t, dur, sr)| (t, dur, sr, label.to_string())) | |
| 265 | - | }) | |
| 295 | + | .filter_map(|f| time_stages(f).map(|(t, dur, sr)| (t, dur, sr, label.to_string()))) | |
| 266 | 296 | .collect(); | |
| 267 | 297 | all_timings.extend(results); | |
| 268 | 298 | } | |
| @@ -313,7 +343,10 @@ fn main() { | |||
| 313 | 343 | ); | |
| 314 | 344 | } | |
| 315 | 345 | ||
| 316 | - | println!(" {:<16} {:>8} {:>8} {:>8} {:>8} {:>8}", "Stage", "Mean", "P50", "P95", "P99", "Max"); | |
| 346 | + | println!( | |
| 347 | + | " {:<16} {:>8} {:>8} {:>8} {:>8} {:>8}", | |
| 348 | + | "Stage", "Mean", "P50", "P95", "P99", "Max" | |
| 349 | + | ); | |
| 317 | 350 | println!(" {}", "─".repeat(58)); | |
| 318 | 351 | stats_line("Decode", &mut decode); | |
| 319 | 352 | stats_line("Loudness+LUFS", &mut loud); | |
| @@ -333,7 +366,10 @@ fn main() { | |||
| 333 | 366 | let realtime_ratio = avg_dur * 1000.0 / avg_total; | |
| 334 | 367 | println!(" Avg sample duration: {:.2}s", avg_dur); | |
| 335 | 368 | println!(" Avg analysis time: {:.1}ms", avg_total); | |
| 336 | - | println!(" Real-time ratio: {:.0}× (analysis is {:.0}× faster than real-time)", realtime_ratio, realtime_ratio); | |
| 369 | + | println!( | |
| 370 | + | " Real-time ratio: {:.0}× (analysis is {:.0}× faster than real-time)", | |
| 371 | + | realtime_ratio, realtime_ratio | |
| 372 | + | ); | |
| 337 | 373 | println!(); | |
| 338 | 374 | ||
| 339 | 375 | // ───────────────────────────────────────────────────────────── | |
| @@ -349,7 +385,10 @@ fn main() { | |||
| 349 | 385 | ("FLAC", test_suite_dir.join("formats/flac")), | |
| 350 | 386 | ]; | |
| 351 | 387 | ||
| 352 | - | println!(" {:<8} {:>6} {:>10} {:>10} {:>10}", "Format", "Files", "Mean(ms)", "P95(ms)", "Max(ms)"); | |
| 388 | + | println!( | |
| 389 | + | " {:<8} {:>6} {:>10} {:>10} {:>10}", | |
| 390 | + | "Format", "Files", "Mean(ms)", "P95(ms)", "Max(ms)" | |
| 391 | + | ); | |
| 353 | 392 | println!(" {}", "─".repeat(50)); | |
| 354 | 393 | ||
| 355 | 394 | for (fmt, dir) in &format_dirs { | |
| @@ -370,7 +409,10 @@ fn main() { | |||
| 370 | 409 | let mean = decode_times.iter().sum::<f64>() / count as f64; | |
| 371 | 410 | let p95 = percentile(&mut decode_times, 95.0); | |
| 372 | 411 | let max = percentile(&mut decode_times, 100.0); | |
| 373 | - | println!(" {:<8} {:>6} {:>10.2} {:>10.2} {:>10.2}", fmt, count, mean, p95, max); | |
| 412 | + | println!( | |
| 413 | + | " {:<8} {:>6} {:>10.2} {:>10.2} {:>10.2}", | |
| 414 | + | fmt, count, mean, p95, max | |
| 415 | + | ); | |
| 374 | 416 | } | |
| 375 | 417 | println!(); | |
| 376 | 418 | ||
| @@ -411,7 +453,10 @@ fn main() { | |||
| 411 | 453 | println!(" Files: {} ({} succeeded)", tp_count, tp_ok); | |
| 412 | 454 | println!(" Wall time: {:.1}s", tp_elapsed); | |
| 413 | 455 | println!(" Throughput: {:.1} files/sec", tp_rate); | |
| 414 | - | println!(" Avg/file: {:.1}ms", tp_elapsed * 1000.0 / tp_ok as f64); | |
| 456 | + | println!( | |
| 457 | + | " Avg/file: {:.1}ms", | |
| 458 | + | tp_elapsed * 1000.0 / tp_ok as f64 | |
| 459 | + | ); | |
| 415 | 460 | println!(); | |
| 416 | 461 | ||
| 417 | 462 | // Single-threaded throughput for comparison | |
| @@ -482,7 +527,10 @@ fn main() { | |||
| 482 | 527 | // Model size | |
| 483 | 528 | let model_path = project_root.join("crates/audiofiles-core/models/layer2_drum.json"); | |
| 484 | 529 | if let Ok(meta) = std::fs::metadata(&model_path) { | |
| 485 | - | println!(" RF model (layer2_drum.json): {:.1} MB on disk, embedded at compile time", meta.len() as f64 / 1_048_576.0); | |
| 530 | + | println!( | |
| 531 | + | " RF model (layer2_drum.json): {:.1} MB on disk, embedded at compile time", | |
| 532 | + | meta.len() as f64 / 1_048_576.0 | |
| 533 | + | ); | |
| 486 | 534 | } | |
| 487 | 535 | println!(); | |
| 488 | 536 | ||
| @@ -492,7 +540,15 @@ fn main() { | |||
| 492 | 540 | println!("━━━ 5. CLASSIFICATION ACCURACY ━━━"); | |
| 493 | 541 | println!(); | |
| 494 | 542 | ||
| 495 | - | let class_dirs = ["kick", "snare", "hihat", "cymbal", "clap", "tom", "percussion"]; | |
| 543 | + | let class_dirs = [ | |
| 544 | + | "kick", | |
| 545 | + | "snare", | |
| 546 | + | "hihat", | |
| 547 | + | "cymbal", | |
| 548 | + | "clap", | |
| 549 | + | "tom", | |
| 550 | + | "percussion", | |
| 551 | + | ]; | |
| 496 | 552 | let max_per_class = 300; // enough for statistical significance, not too slow | |
| 497 | 553 | ||
| 498 | 554 | let mut all_results: Vec<ClassifyResult> = Vec::new(); | |
| @@ -525,7 +581,10 @@ fn main() { | |||
| 525 | 581 | } | |
| 526 | 582 | ||
| 527 | 583 | let total_classified = all_results.len(); | |
| 528 | - | println!(" Evaluated {} samples (up to {} per class)", total_classified, max_per_class); | |
| 584 | + | println!( | |
| 585 | + | " Evaluated {} samples (up to {} per class)", | |
| 586 | + | total_classified, max_per_class | |
| 587 | + | ); | |
| 529 | 588 | println!(); | |
| 530 | 589 | ||
| 531 | 590 | // Strict accuracy: predicted class == expected class exactly | |
| @@ -543,13 +602,22 @@ fn main() { | |||
| 543 | 602 | let drum_acc = drum_correct as f64 / total_classified as f64 * 100.0; | |
| 544 | 603 | ||
| 545 | 604 | println!(" Overall:"); | |
| 546 | - | println!(" Strict accuracy (exact class match): {:.1}% ({}/{})", strict_acc, strict_correct, total_classified); | |
| 547 | - | println!(" Layer 1 accuracy (drum detection): {:.1}% ({}/{})", drum_acc, drum_correct, total_classified); | |
| 605 | + | println!( | |
| 606 | + | " Strict accuracy (exact class match): {:.1}% ({}/{})", | |
| 607 | + | strict_acc, strict_correct, total_classified | |
| 608 | + | ); | |
| 609 | + | println!( | |
| 610 | + | " Layer 1 accuracy (drum detection): {:.1}% ({}/{})", | |
| 611 | + | drum_acc, drum_correct, total_classified | |
| 612 | + | ); | |
| 548 | 613 | println!(); | |
| 549 | 614 | ||
| 550 | 615 | // Per-class breakdown | |
| 551 | 616 | println!(" Per-class (strict):"); | |
| 552 | - | println!(" {:<12} {:>6} {:>8} {:>10} {:>10}", "Expected", "N", "Correct", "Accuracy", "Avg Conf"); | |
| 617 | + | println!( | |
| 618 | + | " {:<12} {:>6} {:>8} {:>10} {:>10}", | |
| 619 | + | "Expected", "N", "Correct", "Accuracy", "Avg Conf" | |
| 620 | + | ); | |
| 553 | 621 | println!(" {}", "─".repeat(52)); | |
| 554 | 622 | ||
| 555 | 623 | for dir_name in &class_dirs { | |
| @@ -565,7 +633,10 @@ fn main() { | |||
| 565 | 633 | continue; | |
| 566 | 634 | } | |
| 567 | 635 | let n = class_results.len(); | |
| 568 | - | let correct = class_results.iter().filter(|r| r.predicted == r.expected).count(); | |
| 636 | + | let correct = class_results | |
| 637 | + | .iter() | |
| 638 | + | .filter(|r| r.predicted == r.expected) | |
| 639 | + | .count(); | |
| 569 | 640 | let acc = correct as f64 / n as f64 * 100.0; | |
| 570 | 641 | let avg_conf = class_results.iter().map(|r| r.confidence).sum::<f64>() / n as f64; | |
| 571 | 642 | println!( | |
| @@ -605,7 +676,10 @@ fn main() { | |||
| 605 | 676 | } | |
| 606 | 677 | print!(" {:>12}", class_names[i]); | |
| 607 | 678 | for pred in &class_ids { | |
| 608 | - | let count = class_results.iter().filter(|r| r.predicted == *pred).count(); | |
| 679 | + | let count = class_results | |
| 680 | + | .iter() | |
| 681 | + | .filter(|r| r.predicted == *pred) | |
| 682 | + | .count(); | |
| 609 | 683 | print!(" {:>7}", count); | |
| 610 | 684 | } | |
| 611 | 685 | let other = class_results | |
| @@ -622,7 +696,11 @@ fn main() { | |||
| 622 | 696 | .filter(|r| !is_drum_class(r.predicted)) | |
| 623 | 697 | .collect(); | |
| 624 | 698 | if !non_drum.is_empty() { | |
| 625 | - | println!(" Samples classified as non-drum: {} ({:.1}%)", non_drum.len(), non_drum.len() as f64 / total_classified as f64 * 100.0); | |
| 699 | + | println!( | |
| 700 | + | " Samples classified as non-drum: {} ({:.1}%)", | |
| 701 | + | non_drum.len(), | |
| 702 | + | non_drum.len() as f64 / total_classified as f64 * 100.0 | |
| 703 | + | ); | |
| 626 | 704 | let mut non_drum_classes: HashMap<&str, usize> = HashMap::new(); | |
| 627 | 705 | for r in &non_drum { | |
| 628 | 706 | *non_drum_classes.entry(r.predicted.as_str()).or_default() += 1; | |
| @@ -643,9 +721,18 @@ fn main() { | |||
| 643 | 721 | ||
| 644 | 722 | let edge_cases = [ | |
| 645 | 723 | ("silent.wav", test_suite_dir.join("edge-cases/silent.wav")), | |
| 646 | - | ("truncated.wav", test_suite_dir.join("edge-cases/truncated.wav")), | |
| 647 | - | ("café-crème.wav", test_suite_dir.join("edge-cases/unicode-name/café-crème.wav")), | |
| 648 | - | ("日本語テスト.wav", test_suite_dir.join("edge-cases/unicode-name/日本語テスト.wav")), | |
| 724 | + | ( | |
| 725 | + | "truncated.wav", | |
| 726 | + | test_suite_dir.join("edge-cases/truncated.wav"), | |
| 727 | + | ), | |
| 728 | + | ( | |
| 729 | + | "café-crème.wav", | |
| 730 | + | test_suite_dir.join("edge-cases/unicode-name/café-crème.wav"), | |
| 731 | + | ), | |
| 732 | + | ( | |
| 733 | + | "日本語テスト.wav", | |
| 734 | + | test_suite_dir.join("edge-cases/unicode-name/日本語テスト.wav"), | |
| 735 | + | ), | |
| 649 | 736 | ]; | |
| 650 | 737 | ||
| 651 | 738 | for (name, path) in &edge_cases { | |
| @@ -708,7 +795,10 @@ fn main() { | |||
| 708 | 795 | println!(); | |
| 709 | 796 | ||
| 710 | 797 | println!("═══════════════════════════════════════════════════════════════"); | |
| 711 | - | println!(" Benchmark complete. {} files analyzed.", n + total_classified); | |
| 798 | + | println!( | |
| 799 | + | " Benchmark complete. {} files analyzed.", | |
| 800 | + | n + total_classified | |
| 801 | + | ); | |
| 712 | 802 | println!("═══════════════════════════════════════════════════════════════"); | |
| 713 | 803 | } | |
| 714 | 804 | ||
| @@ -778,7 +868,9 @@ fn bench_similarity_scaling() { | |||
| 778 | 868 | for _ in 0..runs { | |
| 779 | 869 | let mut best: Vec<f64> = data | |
| 780 | 870 | .iter() | |
| 781 | - | .map(|(_, fv)| audiofiles_core::similarity::feature_distance(probe_fv, fv, &weights)) | |
| 871 | + | .map(|(_, fv)| { | |
| 872 | + | audiofiles_core::similarity::feature_distance(probe_fv, fv, &weights) | |
| 873 | + | }) | |
| 782 | 874 | .collect(); | |
| 783 | 875 | best.sort_by(|a, b| a.total_cmp(b)); | |
| 784 | 876 | let _ = &best[..best.len().min(20)]; | |
| @@ -866,8 +958,7 @@ fn bench_persistence_scaling() { | |||
| 866 | 958 | // offset the hashes so they don't collide with the batched rows | |
| 867 | 959 | insert_analysis_row(&db, size + i); | |
| 868 | 960 | } | |
| 869 | - | let per_row_ms = t.elapsed().as_secs_f64() * 1000.0 | |
| 870 | - | * (size as f64 / per_row_n as f64); // extrapolate to `size` rows | |
| 961 | + | let per_row_ms = t.elapsed().as_secs_f64() * 1000.0 * (size as f64 / per_row_n as f64); // extrapolate to `size` rows | |
| 871 | 962 | ||
| 872 | 963 | // Load from DB (I/O) then build the index (CPU) — the full cold-start cost. | |
| 873 | 964 | let t = Instant::now(); |
| @@ -4,7 +4,6 @@ | |||
| 4 | 4 | use super::*; | |
| 5 | 5 | ||
| 6 | 6 | impl AnalysisBackend for DirectBackend { | |
| 7 | - | ||
| 8 | 7 | // --- Analysis --- | |
| 9 | 8 | ||
| 10 | 9 | fn get_analysis(&self, hash: &str) -> BackendResult<Option<AnalysisResult>> { | |
| @@ -45,9 +44,7 @@ impl AnalysisBackend for DirectBackend { | |||
| 45 | 44 | } | |
| 46 | 45 | } | |
| 47 | 46 | ||
| 48 | - | ||
| 49 | 47 | impl RulesBackend for DirectBackend { | |
| 50 | - | ||
| 51 | 48 | fn list_rules(&self) -> BackendResult<Vec<audiofiles_core::rules::Rule>> { | |
| 52 | 49 | let db = self.db.lock(); | |
| 53 | 50 | Ok(audiofiles_core::rules::list_rules(&db)?) | |
| @@ -95,9 +92,7 @@ impl RulesBackend for DirectBackend { | |||
| 95 | 92 | } | |
| 96 | 93 | } | |
| 97 | 94 | ||
| 98 | - | ||
| 99 | 95 | impl ConfigBackend for DirectBackend { | |
| 100 | - | ||
| 101 | 96 | // --- Config --- | |
| 102 | 97 | ||
| 103 | 98 | fn get_config(&self, key: &str) -> BackendResult<Option<String>> { | |
| @@ -144,4 +139,3 @@ impl ConfigBackend for DirectBackend { | |||
| 144 | 139 | Ok(vfs::get_vfs_sync_files(&db, id)?) | |
| 145 | 140 | } | |
| 146 | 141 | } | |
| 147 | - |
| @@ -4,7 +4,6 @@ | |||
| 4 | 4 | use super::*; | |
| 5 | 5 | ||
| 6 | 6 | impl SearchBackend for DirectBackend { | |
| 7 | - | ||
| 8 | 7 | // --- Search --- | |
| 9 | 8 | ||
| 10 | 9 | fn search_in_folder( | |
| @@ -23,9 +22,7 @@ impl SearchBackend for DirectBackend { | |||
| 23 | 22 | } | |
| 24 | 23 | } | |
| 25 | 24 | ||
| 26 | - | ||
| 27 | 25 | impl ClassifierBackend for DirectBackend { | |
| 28 | - | ||
| 29 | 26 | fn ml_auto_apply_all(&self, k: usize) -> BackendResult<usize> { | |
| 30 | 27 | use audiofiles_core::analysis::{exemplar, trained_head}; | |
| 31 | 28 | let db = self.db.lock(); | |
| @@ -41,20 +38,26 @@ impl ClassifierBackend for DirectBackend { | |||
| 41 | 38 | ||
| 42 | 39 | fn train_classifier_head(&self) -> BackendResult<Option<TrainedHeadInfo>> { | |
| 43 | 40 | let db = self.db.lock(); | |
| 44 | - | Ok(audiofiles_core::analysis::trained_head::train_and_save(&db)?.map(|h| TrainedHeadInfo { | |
| 45 | - | class_count: h.classes.len(), | |
| 46 | - | exemplar_count: h.exemplar_count, | |
| 47 | - | trained_at: h.trained_at, | |
| 48 | - | })) | |
| 41 | + | Ok( | |
| 42 | + | audiofiles_core::analysis::trained_head::train_and_save(&db)?.map(|h| { | |
| 43 | + | TrainedHeadInfo { | |
| 44 | + | class_count: h.classes.len(), | |
| 45 | + | exemplar_count: h.exemplar_count, | |
| 46 | + | trained_at: h.trained_at, | |
| 47 | + | } | |
| 48 | + | }), | |
| 49 | + | ) | |
| 49 | 50 | } | |
| 50 | 51 | ||
| 51 | 52 | fn classifier_head_info(&self) -> BackendResult<Option<TrainedHeadInfo>> { | |
| 52 | 53 | let db = self.db.lock(); | |
| 53 | - | Ok(audiofiles_core::analysis::trained_head::load(&db)?.map(|h| TrainedHeadInfo { | |
| 54 | - | class_count: h.classes.len(), | |
| 55 | - | exemplar_count: h.exemplar_count, | |
| 56 | - | trained_at: h.trained_at, | |
| 57 | - | })) | |
| 54 | + | Ok( | |
| 55 | + | audiofiles_core::analysis::trained_head::load(&db)?.map(|h| TrainedHeadInfo { | |
| 56 | + | class_count: h.classes.len(), | |
| 57 | + | exemplar_count: h.exemplar_count, | |
| 58 | + | trained_at: h.trained_at, | |
| 59 | + | }), | |
| 60 | + | ) | |
| 58 | 61 | } | |
| 59 | 62 | ||
| 60 | 63 | fn clear_classifier_head(&self) -> BackendResult<()> { | |
| @@ -77,10 +80,10 @@ impl ClassifierBackend for DirectBackend { | |||
| 77 | 80 | // the render thread. The UI gates to one active job, so at most one stale | |
| 78 | 81 | // job is reaping at a time. | |
| 79 | 82 | if let Some(old) = self.classifier_worker.lock().take() { | |
| 80 | - | let _ = audiofiles_core::worker_runtime::spawn_detached( | |
| 81 | - | "classifier-reaper", | |
| 82 | - | move || drop(old), | |
| 83 | - | ); | |
| 83 | + | let _ = | |
| 84 | + | audiofiles_core::worker_runtime::spawn_detached("classifier-reaper", move || { | |
| 85 | + | drop(old) | |
| 86 | + | }); | |
| 84 | 87 | } | |
| 85 | 88 | let handle = crate::classifier_worker::spawn_classifier_job(db_path, job) | |
| 86 | 89 | .map_err(|e| BackendError::Other(format!("failed to spawn classifier worker: {e}")))?; | |
| @@ -94,7 +97,9 @@ impl ClassifierBackend for DirectBackend { | |||
| 94 | 97 | opts: &audiofiles_core::analysis::afcl::ExportOptions, | |
| 95 | 98 | ) -> BackendResult<()> { | |
| 96 | 99 | let db = self.db.lock(); | |
| 97 | - | Ok(audiofiles_core::analysis::afcl::export_to_path(&db, path, opts)?) | |
| 100 | + | Ok(audiofiles_core::analysis::afcl::export_to_path( | |
| 101 | + | &db, path, opts, | |
| 102 | + | )?) | |
| 98 | 103 | } | |
| 99 | 104 | ||
| 100 | 105 | fn import_afcl( | |
| @@ -102,7 +107,9 @@ impl ClassifierBackend for DirectBackend { | |||
| 102 | 107 | path: &Path, | |
| 103 | 108 | ) -> BackendResult<audiofiles_core::analysis::afcl::ImportSummary> { | |
| 104 | 109 | let db = self.db.lock(); | |
| 105 | - | Ok(audiofiles_core::analysis::afcl::import_from_path(&db, path)?) | |
| 110 | + | Ok(audiofiles_core::analysis::afcl::import_from_path( | |
| 111 | + | &db, path, | |
| 112 | + | )?) | |
| 106 | 113 | } | |
| 107 | 114 | ||
| 108 | 115 | fn list_classifier_layers( | |
| @@ -119,12 +126,16 @@ impl ClassifierBackend for DirectBackend { | |||
| 119 | 126 | ||
| 120 | 127 | fn set_classifier_layer_enabled(&self, id: &str, enabled: bool) -> BackendResult<()> { | |
| 121 | 128 | let db = self.db.lock(); | |
| 122 | - | Ok(audiofiles_core::analysis::afcl::set_layer_enabled(&db, id, enabled)?) | |
| 129 | + | Ok(audiofiles_core::analysis::afcl::set_layer_enabled( | |
| 130 | + | &db, id, enabled, | |
| 131 | + | )?) | |
| 123 | 132 | } | |
| 124 | 133 | ||
| 125 | 134 | fn set_classifier_layer_weight(&self, id: &str, weight: f64) -> BackendResult<()> { | |
| 126 | 135 | let db = self.db.lock(); | |
| 127 | - | Ok(audiofiles_core::analysis::afcl::set_layer_weight(&db, id, weight)?) | |
| 136 | + | Ok(audiofiles_core::analysis::afcl::set_layer_weight( | |
| 137 | + | &db, id, weight, | |
| 138 | + | )?) | |
| 128 | 139 | } | |
| 129 | 140 | ||
| 130 | 141 | fn accept_ml_tag(&self, hash: &str, tag: &str) -> BackendResult<()> { | |
| @@ -146,7 +157,9 @@ impl ClassifierBackend for DirectBackend { | |||
| 146 | 157 | #[instrument(skip(self))] | |
| 147 | 158 | fn set_tag_policy(&self, tag: &str, review: f64, auto: f64) -> BackendResult<()> { | |
| 148 | 159 | let db = self.db.lock(); | |
| 149 | - | Ok(audiofiles_core::analysis::exemplar::set_policy(&db, tag, review, auto)?) | |
| 160 | + | Ok(audiofiles_core::analysis::exemplar::set_policy( | |
| 161 | + | &db, tag, review, auto, | |
| 162 | + | )?) | |
| 150 | 163 | } | |
| 151 | 164 | ||
| 152 | 165 | fn list_tag_policies( | |
| @@ -166,19 +179,21 @@ impl ClassifierBackend for DirectBackend { | |||
| 166 | 179 | ||
| 167 | 180 | fn apply_cluster_tag(&self, members: &[String], tag: &str) -> BackendResult<usize> { | |
| 168 | 181 | let db = self.db.lock(); | |
| 169 | - | Ok(audiofiles_core::analysis::cluster::apply_cluster_tag(&db, members, tag)?) | |
| 182 | + | Ok(audiofiles_core::analysis::cluster::apply_cluster_tag( | |
| 183 | + | &db, members, tag, | |
| 184 | + | )?) | |
| 170 | 185 | } | |
| 171 | 186 | ||
| 172 | - | fn harvest_folder_labels( | |
| 173 | - | &self, | |
| 174 | - | ) -> BackendResult<Vec<audiofiles_core::harvest::FolderLabel>> { | |
| 187 | + | fn harvest_folder_labels(&self) -> BackendResult<Vec<audiofiles_core::harvest::FolderLabel>> { | |
| 175 | 188 | let db = self.db.lock(); | |
| 176 | 189 | Ok(audiofiles_core::harvest::harvest_folder_labels(&db)?) | |
| 177 | 190 | } | |
| 178 | 191 | ||
| 179 | 192 | fn apply_harvested_tag(&self, hashes: &[String], tag: &str) -> BackendResult<usize> { | |
| 180 | 193 | let db = self.db.lock(); | |
| 181 | - | Ok(audiofiles_core::harvest::apply_harvested_tag(&db, hashes, tag)?) | |
| 194 | + | Ok(audiofiles_core::harvest::apply_harvested_tag( | |
| 195 | + | &db, hashes, tag, | |
| 196 | + | )?) | |
| 182 | 197 | } | |
| 183 | 198 | ||
| 184 | 199 | fn remove_tags_by_source(&self, source: &str) -> BackendResult<usize> { | |
| @@ -188,13 +203,13 @@ impl ClassifierBackend for DirectBackend { | |||
| 188 | 203 | ||
| 189 | 204 | fn get_waveform(&self, hash: &str) -> BackendResult<Option<WaveformData>> { | |
| 190 | 205 | let db = self.db.lock(); | |
| 191 | - | Ok(audiofiles_core::analysis::waveform::load_waveform(&db, hash)) | |
| 206 | + | Ok(audiofiles_core::analysis::waveform::load_waveform( | |
| 207 | + | &db, hash, | |
| 208 | + | )) | |
| 192 | 209 | } | |
| 193 | 210 | } | |
| 194 | 211 | ||
| 195 | - | ||
| 196 | 212 | impl SimilarityBackend for DirectBackend { | |
| 197 | - | ||
| 198 | 213 | // --- Similarity --- | |
| 199 | 214 | ||
| 200 | 215 | fn start_find_similar(&self, hash: &str, limit: usize) -> BackendResult<()> { | |
| @@ -211,7 +226,9 @@ impl SimilarityBackend for DirectBackend { | |||
| 211 | 226 | // spin the repaint-while-busy guard forever. | |
| 212 | 227 | if !delivered { | |
| 213 | 228 | *guard = None; | |
| 214 | - | return Err(BackendError::Other("search worker is not running".to_string())); | |
| 229 | + | return Err(BackendError::Other( | |
| 230 | + | "search worker is not running".to_string(), | |
| 231 | + | )); | |
| 215 | 232 | } | |
| 216 | 233 | Ok(()) | |
| 217 | 234 | } | |
| @@ -227,9 +244,10 @@ impl SimilarityBackend for DirectBackend { | |||
| 227 | 244 | }); | |
| 228 | 245 | if !delivered { | |
| 229 | 246 | *guard = None; | |
| 230 | - | return Err(BackendError::Other("search worker is not running".to_string())); | |
| 247 | + | return Err(BackendError::Other( | |
| 248 | + | "search worker is not running".to_string(), | |
| 249 | + | )); | |
| 231 | 250 | } | |
| 232 | 251 | Ok(()) | |
| 233 | 252 | } | |
| 234 | 253 | } | |
| 235 | - |
| @@ -4,7 +4,6 @@ | |||
| 4 | 4 | use super::*; | |
| 5 | 5 | ||
| 6 | 6 | impl ForgeBackend for DirectBackend { | |
| 7 | - | ||
| 8 | 7 | // --- Sample Forge --- | |
| 9 | 8 | ||
| 10 | 9 | #[instrument(skip_all)] | |
| @@ -34,7 +33,9 @@ impl ForgeBackend for DirectBackend { | |||
| 34 | 33 | // caller doesn't set forge.busy and wedge the repaint guard / busy-guard. | |
| 35 | 34 | if !delivered { | |
| 36 | 35 | *guard = None; | |
| 37 | - | return Err(BackendError::Other("forge worker is not running".to_string())); | |
| 36 | + | return Err(BackendError::Other( | |
| 37 | + | "forge worker is not running".to_string(), | |
| 38 | + | )); | |
| 38 | 39 | } | |
| 39 | 40 | Ok(()) | |
| 40 | 41 | } | |
| @@ -123,11 +124,9 @@ impl ForgeBackend for DirectBackend { | |||
| 123 | 124 | { | |
| 124 | 125 | let mut guard = self.forge_worker.lock(); | |
| 125 | 126 | if guard.is_none() { | |
| 126 | - | *guard = Some( | |
| 127 | - | audiofiles_core::forge::spawn_forge_worker().map_err(|e| { | |
| 128 | - | BackendError::Other(format!("failed to spawn forge worker: {e}")) | |
| 129 | - | })?, | |
| 130 | - | ); | |
| 127 | + | *guard = Some(audiofiles_core::forge::spawn_forge_worker().map_err(|e| { | |
| 128 | + | BackendError::Other(format!("failed to spawn forge worker: {e}")) | |
| 129 | + | })?); | |
| 131 | 130 | } | |
| 132 | 131 | let handle = guard.as_ref().expect("forge worker present"); | |
| 133 | 132 | let _ = handle.send(ForgeCommand::Cancel); | |
| @@ -138,7 +137,9 @@ impl ForgeBackend for DirectBackend { | |||
| 138 | 137 | }); | |
| 139 | 138 | if !delivered { | |
| 140 | 139 | *guard = None; | |
| 141 | - | return Err(BackendError::Other("forge worker is not running".to_string())); | |
| 140 | + | return Err(BackendError::Other( | |
| 141 | + | "forge worker is not running".to_string(), | |
| 142 | + | )); | |
| 142 | 143 | } | |
| 143 | 144 | } | |
| 144 | 145 | *self.forge_pending.lock() = Some(ForgePending::Chop { vfs_id, parent_id }); | |
| @@ -175,11 +176,9 @@ impl ForgeBackend for DirectBackend { | |||
| 175 | 176 | { | |
| 176 | 177 | let mut guard = self.forge_worker.lock(); | |
| 177 | 178 | if guard.is_none() { | |
| 178 | - | *guard = Some( | |
| 179 | - | audiofiles_core::forge::spawn_forge_worker().map_err(|e| { | |
| 180 | - | BackendError::Other(format!("failed to spawn forge worker: {e}")) | |
| 181 | - | })?, | |
| 182 | - | ); | |
| 179 | + | *guard = Some(audiofiles_core::forge::spawn_forge_worker().map_err(|e| { | |
| 180 | + | BackendError::Other(format!("failed to spawn forge worker: {e}")) | |
| 181 | + | })?); | |
| 183 | 182 | } | |
| 184 | 183 | let handle = guard.as_ref().expect("forge worker present"); | |
| 185 | 184 | let _ = handle.send(ForgeCommand::Cancel); | |
| @@ -191,7 +190,9 @@ impl ForgeBackend for DirectBackend { | |||
| 191 | 190 | }); | |
| 192 | 191 | if !delivered { | |
| 193 | 192 | *guard = None; | |
| 194 | - | return Err(BackendError::Other("forge worker is not running".to_string())); | |
| 193 | + | return Err(BackendError::Other( | |
| 194 | + | "forge worker is not running".to_string(), | |
| 195 | + | )); | |
| 195 | 196 | } | |
| 196 | 197 | } | |
| 197 | 198 | *self.forge_pending.lock() = Some(ForgePending::Conform { | |
| @@ -203,4 +204,3 @@ impl ForgeBackend for DirectBackend { | |||
| 203 | 204 | Ok(()) | |
| 204 | 205 | } | |
| 205 | 206 | } | |
| 206 | - |
| @@ -67,7 +67,13 @@ impl VfsBackend for DirectBackend { | |||
| 67 | 67 | sample_hash: &str, | |
| 68 | 68 | ) -> BackendResult<NodeId> { | |
| 69 | 69 | let db = self.db.lock(); | |
| 70 | - | Ok(vfs::create_sample_link(&db, vfs_id, parent_id, name, sample_hash)?) | |
| 70 | + | Ok(vfs::create_sample_link( | |
| 71 | + | &db, | |
| 72 | + | vfs_id, | |
| 73 | + | parent_id, | |
| 74 | + | name, | |
| 75 | + | sample_hash, | |
| 76 | + | )?) | |
| 71 | 77 | } | |
| 72 | 78 | ||
| 73 | 79 | fn get_node(&self, id: NodeId) -> BackendResult<VfsNode> { | |
| @@ -124,9 +130,7 @@ impl VfsBackend for DirectBackend { | |||
| 124 | 130 | } | |
| 125 | 131 | } | |
| 126 | 132 | ||
| 127 | - | ||
| 128 | 133 | impl TagBackend for DirectBackend { | |
| 129 | - | ||
| 130 | 134 | // --- Tags --- | |
| 131 | 135 | ||
| 132 | 136 | #[instrument(skip(self))] | |
| @@ -179,9 +183,7 @@ impl TagBackend for DirectBackend { | |||
| 179 | 183 | } | |
| 180 | 184 | } | |
| 181 | 185 | ||
| 182 | - | ||
| 183 | 186 | impl CollectionBackend for DirectBackend { | |
| 184 | - | ||
| 185 | 187 | // --- Collections --- | |
| 186 | 188 | ||
| 187 | 189 | fn list_collections(&self) -> BackendResult<Vec<Collection>> { | |
| @@ -190,12 +192,20 @@ impl CollectionBackend for DirectBackend { | |||
| 190 | 192 | } | |
| 191 | 193 | ||
| 192 | 194 | #[instrument(skip(self))] | |
| 193 | - | fn create_collection(&self, name: &str, description: Option<&str>) -> BackendResult<CollectionId> { | |
| 195 | + | fn create_collection( | |
| 196 | + | &self, | |
| 197 | + | name: &str, | |
| 198 | + | description: Option<&str>, | |
| 199 | + | ) -> BackendResult<CollectionId> { | |
| 194 | 200 | let db = self.db.lock(); | |
| 195 | 201 | Ok(collections::create_collection(&db, name, description)?) | |
| 196 | 202 | } | |
| 197 | 203 | ||
| 198 | - | fn create_dynamic_collection(&self, name: &str, filter: &SearchFilter) -> BackendResult<CollectionId> { | |
| 204 | + | fn create_dynamic_collection( | |
| 205 | + | &self, | |
| 206 | + | name: &str, | |
| 207 | + | filter: &SearchFilter, | |
| 208 | + | ) -> BackendResult<CollectionId> { | |
| 199 | 209 | let db = self.db.lock(); | |
| 200 | 210 | Ok(collections::create_dynamic_collection(&db, name, filter)?) | |
| 201 | 211 | } | |
| @@ -211,14 +221,30 @@ impl CollectionBackend for DirectBackend { | |||
| 211 | 221 | Ok(collections::delete_collection(&db, id)?) | |
| 212 | 222 | } | |
| 213 | 223 | ||
| 214 | - | fn add_to_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()> { | |
| 224 | + | fn add_to_collection( | |
| 225 | + | &self, | |
| 226 | + | collection_id: CollectionId, | |
| 227 | + | sample_hash: &str, | |
| 228 | + | ) -> BackendResult<()> { | |
| 215 | 229 | let db = self.db.lock(); | |
| 216 | - | Ok(collections::add_to_collection(&db, collection_id, sample_hash)?) | |
| 230 | + | Ok(collections::add_to_collection( | |
| 231 | + | &db, | |
| 232 | + | collection_id, | |
| 233 | + | sample_hash, | |
| 234 | + | )?) | |
| 217 | 235 | } | |
| 218 | 236 | ||
| 219 | - | fn remove_from_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()> { | |
| 237 | + | fn remove_from_collection( | |
| 238 | + | &self, | |
| 239 | + | collection_id: CollectionId, | |
| 240 | + | sample_hash: &str, | |
| 241 | + | ) -> BackendResult<()> { | |
| 220 | 242 | let db = self.db.lock(); | |
| 221 | - | Ok(collections::remove_from_collection(&db, collection_id, sample_hash)?) | |
| 243 | + | Ok(collections::remove_from_collection( | |
| 244 | + | &db, | |
| 245 | + | collection_id, | |
| 246 | + | sample_hash, | |
| 247 | + | )?) | |
| 222 | 248 | } | |
| 223 | 249 | ||
| 224 | 250 | fn list_collection_members(&self, collection_id: CollectionId) -> BackendResult<Vec<String>> { | |
| @@ -231,4 +257,3 @@ impl CollectionBackend for DirectBackend { | |||
| 231 | 257 | Ok(collections::get_sample_collections(&db, sample_hash)?) | |
| 232 | 258 | } | |
| 233 | 259 | } | |
| 234 | - |
| @@ -8,31 +8,32 @@ use std::path::{Path, PathBuf}; | |||
| 8 | 8 | ||
| 9 | 9 | use tracing::instrument; | |
| 10 | 10 | ||
| 11 | + | use audiofiles_core::analysis::AnalysisResult; | |
| 11 | 12 | use audiofiles_core::analysis::config::AnalysisConfig; | |
| 12 | 13 | use audiofiles_core::analysis::waveform::WaveformData; | |
| 13 | - | use audiofiles_core::analysis::AnalysisResult; | |
| 14 | + | use audiofiles_core::collections::Collection; | |
| 14 | 15 | use audiofiles_core::db::Database; | |
| 15 | 16 | use audiofiles_core::edit::EditOperation; | |
| 16 | 17 | use audiofiles_core::edit::worker::{EditCommand, EditEvent, EditWorkerHandle}; | |
| 17 | - | use audiofiles_core::export::profile::DeviceProfileSummary; | |
| 18 | 18 | use audiofiles_core::export::ExportItem; | |
| 19 | + | use audiofiles_core::export::profile::DeviceProfileSummary; | |
| 19 | 20 | use audiofiles_core::forge::{ChopMethod, ConformResult, ConformTarget}; | |
| 20 | 21 | use audiofiles_core::search::SearchFilter; | |
| 21 | - | use audiofiles_core::collections::Collection; | |
| 22 | 22 | use audiofiles_core::store::SampleStore; | |
| 23 | 23 | use audiofiles_core::vfs::{self, Vfs, VfsNode, VfsNodeWithAnalysis}; | |
| 24 | - | use audiofiles_core::{collections, search, tags, CollectionId, NodeId, VfsId}; | |
| 24 | + | use audiofiles_core::{CollectionId, NodeId, VfsId, collections, search, tags}; | |
| 25 | 25 | ||
| 26 | 26 | use super::search_worker::{self, SearchCommand, SearchEvent}; | |
| 27 | 27 | use parking_lot::Mutex; | |
| 28 | 28 | ||
| 29 | 29 | use super::{ | |
| 30 | - | Backend, VfsBackend, TagBackend, SearchBackend, CollectionBackend, AnalysisBackend, RulesBackend, ClassifierBackend, SimilarityBackend, StoreBackend, ExportBackend, ForgeBackend, ConfigBackend, OperationsBackend, | |
| 31 | - | BackendError, BackendEvent, BackendResult, ExportConfigDesc, ExportItemDesc, | |
| 32 | - | ImportStrategyDesc, ImportedFolderDesc, TrainedHeadInfo, | |
| 30 | + | AnalysisBackend, Backend, BackendError, BackendEvent, BackendResult, ClassifierBackend, | |
| 31 | + | CollectionBackend, ConfigBackend, ExportBackend, ExportConfigDesc, ExportItemDesc, | |
| 32 | + | ForgeBackend, ImportStrategyDesc, ImportedFolderDesc, OperationsBackend, RulesBackend, | |
| 33 | + | SearchBackend, SimilarityBackend, StoreBackend, TagBackend, TrainedHeadInfo, VfsBackend, | |
| 33 | 34 | }; | |
| 34 | 35 | ||
| 35 | - | use super::forge::{ForgePending, ForgeImportOutcome, forge_import_chop, forge_import_conform}; | |
| 36 | + | use super::forge::{ForgeImportOutcome, ForgePending, forge_import_chop, forge_import_conform}; | |
| 36 | 37 | ||
| 37 | 38 | use crate::cleanup::{CleanupCommand, CleanupHandle}; | |
| 38 | 39 | use crate::export::{ExportCommand, ExportHandle}; | |
| @@ -117,7 +118,9 @@ impl DirectBackend { | |||
| 117 | 118 | // should never trip — but if it does, the conform/M8/MPC wedge | |
| 118 | 119 | // would silently show "No device profiles". Log so the empty | |
| 119 | 120 | // registry is diagnosable instead of mysterious. | |
| 120 | - | tracing::error!("Failed to load device-profile registry, falling back to empty: {e}"); | |
| 121 | + | tracing::error!( | |
| 122 | + | "Failed to load device-profile registry, falling back to empty: {e}" | |
| 123 | + | ); | |
| 121 | 124 | audiofiles_rhai::registry::PluginRegistry::new() | |
| 122 | 125 | }), | |
| 123 | 126 | } | |
| @@ -146,16 +149,18 @@ impl DirectBackend { | |||
| 146 | 149 | db_path, | |
| 147 | 150 | self.store.root().to_path_buf(), | |
| 148 | 151 | ) | |
| 149 | - | .map_err(|e| { | |
| 150 | - | BackendError::Other(format!("failed to spawn loose-files worker: {e}")) | |
| 151 | - | })?; | |
| 152 | + | .map_err(|e| BackendError::Other(format!("failed to spawn loose-files worker: {e}")))?; | |
| 152 | 153 | *guard = Some(handle); | |
| 153 | 154 | } | |
| 154 | 155 | // If the cached worker died (e.g. its DB open failed inside the thread), | |
| 155 | 156 | // send returns false. Drop the dead handle so the next call respawns, and | |
| 156 | 157 | // report Err so the caller never sets a busy flag for a command that no | |
| 157 | 158 | // worker will ever answer (which would wedge the repaint-while-busy guard). | |
| 158 | - | if !guard.as_ref().expect("loose-files worker present").send(cmd) { | |
| 159 | + | if !guard | |
| 160 | + | .as_ref() | |
| 161 | + | .expect("loose-files worker present") | |
| 162 | + | .send(cmd) | |
| 163 | + | { | |
| 159 | 164 | *guard = None; | |
| 160 | 165 | return Err(BackendError::Other( | |
| 161 | 166 | "loose-files worker is not running".to_string(), | |
| @@ -219,9 +224,10 @@ impl DirectBackend { | |||
| 219 | 224 | ||
| 220 | 225 | // Format: if Original, set to profile's first supported format | |
| 221 | 226 | if config.format == ExportFormat::Original | |
| 222 | - | && let Some(fmt) = profile.audio.formats.first() { | |
| 223 | - | config.format = fmt.clone(); | |
| 224 | - | } | |
| 227 | + | && let Some(fmt) = profile.audio.formats.first() | |
| 228 | + | { | |
| 229 | + | config.format = fmt.clone(); | |
| 230 | + | } | |
| 225 | 231 | ||
| 226 | 232 | // Sample rate: if not set, use profile's first rate | |
| 227 | 233 | if config.sample_rate.is_none() { | |
| @@ -252,7 +258,10 @@ impl DirectBackend { | |||
| 252 | 258 | // to avoid holding the DB lock during potentially slow Rhai execution. | |
| 253 | 259 | let mut infos: Vec<_> = { | |
| 254 | 260 | let db = self.db.lock(); | |
| 255 | - | items.iter().map(|item| super::sample_info::build_sample_info(&db, &self.store, item)).collect() | |
| 261 | + | items | |
| 262 | + | .iter() | |
| 263 | + | .map(|item| super::sample_info::build_sample_info(&db, &self.store, item)) | |
| 264 | + | .collect() | |
| 256 | 265 | }; | |
| 257 | 266 | // Probe bit depth off the DB lock and in parallel: N independent | |
| 258 | 267 | // file-header reads that previously ran serially *while holding the | |
| @@ -265,7 +274,10 @@ impl DirectBackend { | |||
| 265 | 274 | .collect(); | |
| 266 | 275 | let depths: Vec<u16> = paths | |
| 267 | 276 | .into_par_iter() | |
| 268 | - | .map(|p| p.and_then(|p| super::sample_info::probe_bit_depth(&p)).unwrap_or(0)) | |
| 277 | + | .map(|p| { | |
| 278 | + | p.and_then(|p| super::sample_info::probe_bit_depth(&p)) | |
| 279 | + | .unwrap_or(0) | |
| 280 | + | }) | |
| 269 | 281 | .collect(); | |
| 270 | 282 | for (info, depth) in infos.iter_mut().zip(depths) { | |
| 271 | 283 | info.bit_depth = depth; | |
| @@ -292,7 +304,11 @@ impl DirectBackend { | |||
| 292 | 304 | }; | |
| 293 | 305 | } | |
| 294 | 306 | let mut ki = 0; | |
| 295 | - | items.retain(|_| { let k = keep[ki]; ki += 1; k }); | |
| 307 | + | items.retain(|_| { | |
| 308 | + | let k = keep[ki]; | |
| 309 | + | ki += 1; | |
| 310 | + | k | |
| 311 | + | }); | |
| 296 | 312 | } | |
| 297 | 313 | ||
| 298 | 314 | // transform_filename hook: pre-compute output names with custom naming logic | |
| @@ -302,11 +318,8 @@ impl DirectBackend { | |||
| 302 | 318 | .as_ref() | |
| 303 | 319 | .and_then(|p| audiofiles_core::rename::RenamePattern::parse(p).ok()); | |
| 304 | 320 | ||
| 305 | - | let mut names = audiofiles_core::export::resolve_output_names( | |
| 306 | - | items, | |
| 307 | - | config, | |
| 308 | - | pattern.as_ref(), | |
| 309 | - | ); | |
| 321 | + | let mut names = | |
| 322 | + | audiofiles_core::export::resolve_output_names(items, config, pattern.as_ref()); | |
| 310 | 323 | ||
| 311 | 324 | let device_name = profile.name.clone(); | |
| 312 | 325 | let destination = config.destination.display().to_string(); | |
| @@ -332,7 +345,8 @@ impl DirectBackend { | |||
| 332 | 345 | // to prevent traversal. resolve_output_names re-sanitizes | |
| 333 | 346 | // overrides too, so this is a first layer, not the only one. | |
| 334 | 347 | let safe_stem = new_stem.replace(['/', '\\', '\0'], "_"); | |
| 335 | - | let safe_stem = if safe_stem.is_empty() || safe_stem == ".." || safe_stem == "." { | |
| 348 | + | let safe_stem = if safe_stem.is_empty() || safe_stem == ".." || safe_stem == "." | |
| 349 | + | { | |
| 336 | 350 | "untitled".to_string() | |
| 337 | 351 | } else { | |
| 338 | 352 | safe_stem | |
| @@ -355,12 +369,12 @@ use audiofiles_core::util::split_name_ext; | |||
| 355 | 369 | ||
| 356 | 370 | impl Backend for DirectBackend {} | |
| 357 | 371 | ||
| 358 | - | mod library; | |
| 359 | 372 | mod analysis; | |
| 360 | - | mod store; | |
| 361 | 373 | mod classify; | |
| 362 | 374 | mod forge; | |
| 375 | + | mod library; | |
| 363 | 376 | mod operations; | |
| 377 | + | mod store; | |
| 364 | 378 | ||
| 365 | 379 | #[cfg(test)] | |
| 366 | 380 | mod tests; |
| @@ -5,7 +5,6 @@ | |||
| 5 | 5 | use super::*; | |
| 6 | 6 | ||
| 7 | 7 | impl OperationsBackend for DirectBackend { | |
| 8 | - | ||
| 9 | 8 | // --- VFS Mirror --- | |
| 10 | 9 | ||
| 11 | 10 | fn sync_vfs_mirror(&self, mirror_root: &Path) -> BackendResult<(usize, usize, usize)> { | |
| @@ -15,16 +14,16 @@ impl OperationsBackend for DirectBackend { | |||
| 15 | 14 | store_root: self.store.root().to_path_buf(), | |
| 16 | 15 | }; | |
| 17 | 16 | let stats = audiofiles_core::vfs_mirror::sync_mirror(&db, &config)?; | |
| 18 | - | Ok((stats.dirs_created, stats.links_created, stats.entries_removed)) | |
| 17 | + | Ok(( | |
| 18 | + | stats.dirs_created, | |
| 19 | + | stats.links_created, | |
| 20 | + | stats.entries_removed, | |
| 21 | + | )) | |
| 19 | 22 | } | |
| 20 | 23 | ||
| 21 | 24 | // --- Long-running operations --- | |
| 22 | 25 | ||
| 23 | - | fn start_import( | |
| 24 | - | &self, | |
| 25 | - | source: &Path, | |
| 26 | - | strategy: ImportStrategyDesc, | |
| 27 | - | ) -> BackendResult<()> { | |
| 26 | + | fn start_import(&self, source: &Path, strategy: ImportStrategyDesc) -> BackendResult<()> { | |
| 28 | 27 | let import_strategy = resolve_import_strategy(strategy); | |
| 29 | 28 | let handle = self.respawn_import_worker()?; | |
| 30 | 29 | handle.send(ImportCommand::ImportDirectory { | |
| @@ -60,9 +59,9 @@ impl OperationsBackend for DirectBackend { | |||
| 60 | 59 | sample_hashes | |
| 61 | 60 | .into_iter() | |
| 62 | 61 | .filter_map(|(hash, ext)| { | |
| 63 | - | let path = audiofiles_core::store::resolve_file_path( | |
| 64 | - | &self.store, &db, &hash, &ext, | |
| 65 | - | ).ok()?; | |
| 62 | + | let path = | |
| 63 | + | audiofiles_core::store::resolve_file_path(&self.store, &db, &hash, &ext) | |
| 64 | + | .ok()?; | |
| 66 | 65 | if path.exists() { | |
| 67 | 66 | Some((hash, ext, path)) | |
| 68 | 67 | } else { | |
| @@ -210,11 +209,7 @@ impl OperationsBackend for DirectBackend { | |||
| 210 | 209 | Ok(()) | |
| 211 | 210 | } | |
| 212 | 211 | ||
| 213 | - | fn delete_edit_history( | |
| 214 | - | &self, | |
| 215 | - | source_hash: &str, | |
| 216 | - | result_hash: &str, | |
| 217 | - | ) -> BackendResult<()> { | |
| 212 | + | fn delete_edit_history(&self, source_hash: &str, result_hash: &str) -> BackendResult<()> { | |
| 218 | 213 | let db = self.db.lock(); | |
| 219 | 214 | db.conn() | |
| 220 | 215 | .execute( | |
| @@ -233,11 +228,16 @@ impl OperationsBackend for DirectBackend { | |||
| 233 | 228 | ||
| 234 | 229 | fn storage_stats(&self) -> BackendResult<crate::backend::StorageStats> { | |
| 235 | 230 | let db = self.db.lock(); | |
| 236 | - | let (sample_count, total_bytes) = db.storage_stats() | |
| 231 | + | let (sample_count, total_bytes) = db | |
| 232 | + | .storage_stats() | |
| 237 | 233 | .map_err(|e| BackendError::Other(e.to_string()))?; | |
| 238 | 234 | let db_path = self.data_dir.join("audiofiles.db"); | |
| 239 | 235 | let db_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0); | |
| 240 | - | Ok(crate::backend::StorageStats { sample_count, total_bytes, db_bytes }) | |
| 236 | + | Ok(crate::backend::StorageStats { | |
| 237 | + | sample_count, | |
| 238 | + | total_bytes, | |
| 239 | + | db_bytes, | |
| 240 | + | }) | |
| 241 | 241 | } | |
| 242 | 242 | ||
| 243 | 243 | fn vfs_storage_stats(&self, vfs_id: audiofiles_core::VfsId) -> BackendResult<(u64, u64)> { | |
| @@ -493,7 +493,12 @@ impl OperationsBackend for DirectBackend { | |||
| 493 | 493 | }, | |
| 494 | 494 | move || { | |
| 495 | 495 | forge_import_conform( | |
| 496 | - | &data_dir, &root, vfs_id, parent_id, sample_rate, bit_depth, | |
| 496 | + | &data_dir, | |
| 497 | + | &root, | |
| 498 | + | vfs_id, | |
| 499 | + | parent_id, | |
| 500 | + | sample_rate, | |
| 501 | + | bit_depth, | |
| 497 | 502 | staging, | |
| 498 | 503 | ) | |
| 499 | 504 | }, | |
| @@ -530,13 +535,15 @@ impl OperationsBackend for DirectBackend { | |||
| 530 | 535 | ForgeImportOutcome::Chop { slice_count } => { | |
| 531 | 536 | events.push(BackendEvent::ForgeChopComplete { slice_count }) | |
| 532 | 537 | } | |
| 533 | - | ForgeImportOutcome::Conform { sample_rate, bit_depth, overshoot } => { | |
| 534 | - | events.push(BackendEvent::ForgeConformComplete { | |
| 535 | - | sample_rate, | |
| 536 | - | bit_depth, | |
| 537 | - | overshoot, | |
| 538 | - | }) | |
| 539 | - | } | |
| 538 | + | ForgeImportOutcome::Conform { | |
| 539 | + | sample_rate, | |
| 540 | + | bit_depth, | |
| 541 | + | overshoot, | |
| 542 | + | } => events.push(BackendEvent::ForgeConformComplete { | |
| 543 | + | sample_rate, | |
| 544 | + | bit_depth, | |
| 545 | + | overshoot, | |
| 546 | + | }), | |
| 540 | 547 | ForgeImportOutcome::Error { error } => { | |
| 541 | 548 | events.push(BackendEvent::ForgeError { error }) | |
| 542 | 549 | } | |
| @@ -590,4 +597,3 @@ impl OperationsBackend for DirectBackend { | |||
| 590 | 597 | events | |
| 591 | 598 | } | |
| 592 | 599 | } | |
| 593 | - |
| @@ -4,7 +4,6 @@ | |||
| 4 | 4 | use super::*; | |
| 5 | 5 | ||
| 6 | 6 | impl StoreBackend for DirectBackend { | |
| 7 | - | ||
| 8 | 7 | // --- Store --- | |
| 9 | 8 | ||
| 10 | 9 | fn import_file(&self, path: &Path) -> BackendResult<String> { | |
| @@ -14,7 +13,12 @@ impl StoreBackend for DirectBackend { | |||
| 14 | 13 | ||
| 15 | 14 | fn sample_path(&self, hash: &str, ext: &str) -> BackendResult<PathBuf> { | |
| 16 | 15 | let db = self.db.lock(); | |
| 17 | - | Ok(audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)?) | |
| 16 | + | Ok(audiofiles_core::store::resolve_file_path( | |
| 17 | + | &self.store, | |
| 18 | + | &db, | |
| 19 | + | hash, | |
| 20 | + | ext, | |
| 21 | + | )?) | |
| 18 | 22 | } | |
| 19 | 23 | ||
| 20 | 24 | fn sample_extension(&self, hash: &str) -> BackendResult<String> { | |
| @@ -73,7 +77,12 @@ impl StoreBackend for DirectBackend { | |||
| 73 | 77 | ||
| 74 | 78 | fn relocate_sample(&self, hash: &str, new_path: &Path) -> BackendResult<()> { | |
| 75 | 79 | let db = self.db.lock(); | |
| 76 | - | Ok(audiofiles_core::store::relocate_sample(&self.store, &db, hash, new_path)?) | |
| 80 | + | Ok(audiofiles_core::store::relocate_sample( | |
| 81 | + | &self.store, | |
| 82 | + | &db, | |
| 83 | + | hash, | |
| 84 | + | new_path, | |
| 85 | + | )?) | |
| 77 | 86 | } | |
| 78 | 87 | ||
| 79 | 88 | fn check_vault_integrity(&self) -> BackendResult<(usize, usize)> { | |
| @@ -91,7 +100,10 @@ impl StoreBackend for DirectBackend { | |||
| 91 | 100 | search_root: &std::path::Path, | |
| 92 | 101 | ) -> BackendResult<(usize, usize)> { | |
| 93 | 102 | let db = self.db.lock(); | |
| 94 | - | Ok(audiofiles_core::store::relocate_missing_loose_files(&db, search_root)?) | |
| 103 | + | Ok(audiofiles_core::store::relocate_missing_loose_files( | |
| 104 | + | &db, | |
| 105 | + | search_root, | |
| 106 | + | )?) | |
| 95 | 107 | } | |
| 96 | 108 | ||
| 97 | 109 | fn start_loose_files_check(&self) -> BackendResult<()> { | |
| @@ -113,9 +125,7 @@ impl StoreBackend for DirectBackend { | |||
| 113 | 125 | } | |
| 114 | 126 | } | |
| 115 | 127 | ||
| 116 | - | ||
| 117 | 128 | impl ExportBackend for DirectBackend { | |
| 118 | - | ||
| 119 | 129 | // --- Export --- | |
| 120 | 130 | ||
| 121 | 131 | fn collect_export_items( | |
| @@ -124,7 +134,9 @@ impl ExportBackend for DirectBackend { | |||
| 124 | 134 | parent_id: Option<NodeId>, | |
| 125 | 135 | ) -> BackendResult<Vec<ExportItem>> { | |
| 126 | 136 | let db = self.db.lock(); | |
| 127 | - | Ok(audiofiles_core::export::collect_export_items(&db, vfs_id, parent_id)?) | |
| 137 | + | Ok(audiofiles_core::export::collect_export_items( | |
| 138 | + | &db, vfs_id, parent_id, | |
| 139 | + | )?) | |
| 128 | 140 | } | |
| 129 | 141 | ||
| 130 | 142 | fn enrich_export_with_tags(&self, items: &mut [ExportItem]) -> BackendResult<()> { | |
| @@ -165,4 +177,3 @@ impl ExportBackend for DirectBackend { | |||
| 165 | 177 | } | |
| 166 | 178 | } | |
| 167 | 179 | } | |
| 168 | - |
| @@ -143,7 +143,9 @@ fn list_all_directories_works() { | |||
| 143 | 143 | let backend = setup(); | |
| 144 | 144 | let vfs_id = backend.create_vfs("Test").unwrap(); | |
| 145 | 145 | let drums = backend.create_directory(vfs_id, None, "Drums").unwrap(); | |
| 146 | - | backend.create_directory(vfs_id, Some(drums), "Kicks").unwrap(); | |
| 146 | + | backend | |
| 147 | + | .create_directory(vfs_id, Some(drums), "Kicks") | |
| 148 | + | .unwrap(); | |
| 147 | 149 | ||
| 148 | 150 | let dirs = backend.list_all_directories(vfs_id).unwrap(); | |
| 149 | 151 | assert_eq!(dirs.len(), 2); | |
| @@ -173,7 +175,10 @@ fn write_float_wav(path: &Path, channels: u16, sample_rate: u32, samples: &[f32] | |||
| 173 | 175 | for &s in samples { | |
| 174 | 176 | buf.extend_from_slice(&s.to_le_bytes()); | |
| 175 | 177 | } | |
| 176 | - | std::fs::File::create(path).unwrap().write_all(&buf).unwrap(); | |
| 178 | + | std::fs::File::create(path) | |
| 179 | + | .unwrap() | |
| 180 | + | .write_all(&buf) | |
| 181 | + | .unwrap(); | |
| 177 | 182 | } | |
| 178 | 183 | ||
| 179 | 184 | #[test] | |
| @@ -212,7 +217,14 @@ fn chop_sample_creates_slice_folder() { | |||
| 212 | 217 | assert_eq!(marks.last().copied(), Some(1.0)); | |
| 213 | 218 | ||
| 214 | 219 | let count = backend | |
| 215 | - | .chop_sample(vfs_id, &hash, "wav", "loop.wav", None, &ChopMethod::EqualDivisions(4)) | |
| 220 | + | .chop_sample( | |
| 221 | + | vfs_id, | |
| 222 | + | &hash, | |
| 223 | + | "wav", | |
| 224 | + | "loop.wav", | |
| 225 | + | None, | |
| 226 | + | &ChopMethod::EqualDivisions(4), | |
| 227 | + | ) | |
| 216 | 228 | .unwrap(); | |
| 217 | 229 | assert_eq!(count, 4); | |
| 218 | 230 | ||
| @@ -242,7 +254,14 @@ fn start_chop_runs_on_worker_and_imports_on_poll() { | |||
| 242 | 254 | ||
| 243 | 255 | // Dispatch to the worker; returns immediately (no UI block). | |
| 244 | 256 | backend | |
| 245 | - | .start_chop(vfs_id, &hash, "wav", "loop.wav", None, &ChopMethod::EqualDivisions(4)) | |
| 257 | + | .start_chop( | |
| 258 | + | vfs_id, | |
| 259 | + | &hash, | |
| 260 | + | "wav", | |
| 261 | + | "loop.wav", | |
| 262 | + | None, | |
| 263 | + | &ChopMethod::EqualDivisions(4), | |
| 264 | + | ) | |
| 246 | 265 | .unwrap(); | |
| 247 | 266 | ||
| 248 | 267 | // Poll until the chop completes (worker does CPU; poll imports). | |
| @@ -274,9 +293,7 @@ fn start_chop_runs_on_worker_and_imports_on_poll() { | |||
| 274 | 293 | fn device_conform_target_resolves_bundled() { | |
| 275 | 294 | let backend = setup(); | |
| 276 | 295 | // A bundled mono device resolves to a mono target. | |
| 277 | - | let target = backend | |
| 278 | - | .device_conform_target("SP-404 MKII", 48000) | |
| 279 | - | .unwrap(); | |
| 296 | + | let target = backend.device_conform_target("SP-404 MKII", 48000).unwrap(); | |
| 280 | 297 | assert!(target.is_some(), "SP-404 MKII should resolve to a target"); | |
| 281 | 298 | } | |
| 282 | 299 |
| @@ -29,13 +29,17 @@ pub(super) enum ForgePending { | |||
| 29 | 29 | /// writes) runs on its own WAL connection so it never holds the GUI's `db.lock()`; | |
| 30 | 30 | /// `poll_workers` drains this and emits the matching `BackendEvent` on a later frame. | |
| 31 | 31 | pub(super) enum ForgeImportOutcome { | |
| 32 | - | Chop { slice_count: usize }, | |
| 32 | + | Chop { | |
| 33 | + | slice_count: usize, | |
| 34 | + | }, | |
| 33 | 35 | Conform { | |
| 34 | 36 | sample_rate: u32, | |
| 35 | 37 | bit_depth: u16, | |
| 36 | 38 | overshoot: Option<super::ForgeOvershoot>, | |
| 37 | 39 | }, | |
| 38 | - | Error { error: String }, | |
| 40 | + | Error { | |
| 41 | + | error: String, | |
| 42 | + | }, | |
| 39 | 43 | } | |
| 40 | 44 | ||
| 41 | 45 | /// Open a fresh WAL connection + store and import chop slices. Runs on a job | |
| @@ -50,15 +54,27 @@ pub(super) fn forge_import_chop( | |||
| 50 | 54 | ) -> ForgeImportOutcome { | |
| 51 | 55 | let db = match Database::open(data_dir.join("audiofiles.db")) { | |
| 52 | 56 | Ok(d) => d, | |
| 53 | - | Err(e) => return ForgeImportOutcome::Error { error: e.to_string() }, | |
| 57 | + | Err(e) => { | |
| 58 | + | return ForgeImportOutcome::Error { | |
| 59 | + | error: e.to_string(), | |
| 60 | + | }; | |
| 61 | + | } | |
| 54 | 62 | }; | |
| 55 | 63 | let store = match SampleStore::new(root) { | |
| 56 | 64 | Ok(s) => s, | |
| 57 | - | Err(e) => return ForgeImportOutcome::Error { error: e.to_string() }, | |
| 65 | + | Err(e) => { | |
| 66 | + | return ForgeImportOutcome::Error { | |
| 67 | + | error: e.to_string(), | |
| 68 | + | }; | |
| 69 | + | } | |
| 58 | 70 | }; | |
| 59 | 71 | match audiofiles_core::forge::import_chop(&store, &db, vfs_id, parent_id, staging) { | |
| 60 | - | Ok(r) => ForgeImportOutcome::Chop { slice_count: r.slice_count }, | |
| 61 | - | Err(e) => ForgeImportOutcome::Error { error: e.to_string() }, | |
| 72 | + | Ok(r) => ForgeImportOutcome::Chop { | |
| 73 | + | slice_count: r.slice_count, | |
| 74 | + | }, | |
| 75 | + | Err(e) => ForgeImportOutcome::Error { | |
| 76 | + | error: e.to_string(), | |
| 77 | + | }, | |
| 62 | 78 | } | |
| 63 | 79 | } | |
| 64 | 80 | ||
| @@ -77,24 +93,43 @@ pub(super) fn forge_import_conform( | |||
| 77 | 93 | ) -> ForgeImportOutcome { | |
| 78 | 94 | let db = match Database::open(data_dir.join("audiofiles.db")) { | |
| 79 | 95 | Ok(d) => d, | |
| 80 | - | Err(e) => return ForgeImportOutcome::Error { error: e.to_string() }, | |
| 96 | + | Err(e) => { | |
| 97 | + | return ForgeImportOutcome::Error { | |
| 98 | + | error: e.to_string(), | |
| 99 | + | }; | |
| 100 | + | } | |
| 81 | 101 | }; | |
| 82 | 102 | let store = match SampleStore::new(root) { | |
| 83 | 103 | Ok(s) => s, | |
| 84 | - | Err(e) => return ForgeImportOutcome::Error { error: e.to_string() }, | |
| 104 | + | Err(e) => { | |
| 105 | + | return ForgeImportOutcome::Error { | |
| 106 | + | error: e.to_string(), | |
| 107 | + | }; | |
| 108 | + | } | |
| 85 | 109 | }; | |
| 86 | 110 | match audiofiles_core::forge::import_conform(&store, &db, vfs_id, parent_id, staging) { | |
| 87 | 111 | Ok(r) => { | |
| 88 | 112 | let overshoot = r.overshoot.map(|o| match o.action { | |
| 89 | 113 | audiofiles_core::forge::OvershootAction::Trimmed { gain_db } => { | |
| 90 | - | super::ForgeOvershoot::Trimmed { peak_dbfs: o.peak_dbfs, gain_db } | |
| 114 | + | super::ForgeOvershoot::Trimmed { | |
| 115 | + | peak_dbfs: o.peak_dbfs, | |
| 116 | + | gain_db, | |
| 117 | + | } | |
| 91 | 118 | } | |
| 92 | 119 | audiofiles_core::forge::OvershootAction::Flagged => { | |
| 93 | - | super::ForgeOvershoot::Flagged { peak_dbfs: o.peak_dbfs } | |
| 120 | + | super::ForgeOvershoot::Flagged { | |
| 121 | + | peak_dbfs: o.peak_dbfs, | |
| 122 | + | } | |
| 94 | 123 | } | |
| 95 | 124 | }); | |
| 96 | - | ForgeImportOutcome::Conform { sample_rate, bit_depth, overshoot } | |
| 125 | + | ForgeImportOutcome::Conform { | |
| 126 | + | sample_rate, | |
| 127 | + | bit_depth, | |
| 128 | + | overshoot, | |
| 129 | + | } | |
| 97 | 130 | } | |
| 98 | - | Err(e) => ForgeImportOutcome::Error { error: e.to_string() }, | |
| 131 | + | Err(e) => ForgeImportOutcome::Error { | |
| 132 | + | error: e.to_string(), | |
| 133 | + | }, | |
| 99 | 134 | } | |
| 100 | 135 | } |
| @@ -8,22 +8,22 @@ | |||
| 8 | 8 | //! - `DaemonBackend` (Phase D) — forwards calls to the daemon over Unix socket via JSON-RPC. | |
| 9 | 9 | ||
| 10 | 10 | pub mod direct; | |
| 11 | - | pub mod search_worker; | |
| 12 | 11 | mod forge; | |
| 13 | 12 | mod sample_info; | |
| 13 | + | pub mod search_worker; | |
| 14 | 14 | ||
| 15 | 15 | use std::path::{Path, PathBuf}; | |
| 16 | 16 | ||
| 17 | + | use audiofiles_core::analysis::AnalysisResult; | |
| 17 | 18 | use audiofiles_core::analysis::config::AnalysisConfig; | |
| 18 | 19 | use audiofiles_core::analysis::suggest::TagSuggestion; | |
| 19 | 20 | use audiofiles_core::analysis::waveform::WaveformData; | |
| 20 | - | use audiofiles_core::analysis::AnalysisResult; | |
| 21 | + | use audiofiles_core::collections::Collection; | |
| 21 | 22 | use audiofiles_core::edit::EditOperation; | |
| 22 | 23 | use audiofiles_core::export::profile::DeviceProfileSummary; | |
| 23 | 24 | use audiofiles_core::export::{ExportConfig, ExportItem}; | |
| 24 | 25 | use audiofiles_core::forge::{ChopMethod, ConformResult, ConformTarget}; | |
| 25 | 26 | use audiofiles_core::search::SearchFilter; | |
| 26 | - | use audiofiles_core::collections::Collection; | |
| 27 | 27 | use audiofiles_core::store::TombstonedSample; | |
| 28 | 28 | use audiofiles_core::vfs::{Vfs, VfsNode, VfsNodeWithAnalysis}; | |
| 29 | 29 | use audiofiles_core::{CollectionId, NodeId, VfsId}; | |
| @@ -207,9 +207,17 @@ pub struct ImportedFolderDesc { | |||
| 207 | 207 | /// Serializable description of an import strategy (for IPC). | |
| 208 | 208 | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | |
| 209 | 209 | pub enum ImportStrategyDesc { | |
| 210 | - | Flat { vfs_id: VfsId, parent_id: Option<NodeId> }, | |
| 211 | - | NewVfs { vfs_name: String }, | |
| 212 | - | MergeIntoVfs { vfs_id: VfsId, parent_id: Option<NodeId> }, | |
| 210 | + | Flat { | |
| 211 | + | vfs_id: VfsId, | |
| 212 | + | parent_id: Option<NodeId>, | |
| 213 | + | }, | |
| 214 | + | NewVfs { | |
| 215 | + | vfs_name: String, | |
| 216 | + | }, | |
| 217 | + | MergeIntoVfs { | |
| 218 | + | vfs_id: VfsId, | |
| 219 | + | parent_id: Option<NodeId>, | |
| 220 | + | }, | |
| 213 | 221 | } | |
| 214 | 222 | ||
| 215 | 223 | /// Serializable description of an export item (for IPC). | |
| @@ -257,7 +265,10 @@ pub enum ClassifierJob { | |||
| 257 | 265 | /// Cluster the library into `k` groups. | |
| 258 | 266 | Cluster { k: usize }, | |
| 259 | 267 | /// Export the user's classifier data to a `.afcl` file. | |
| 260 | - | Export { path: std::path::PathBuf, opts: audiofiles_core::analysis::afcl::ExportOptions }, | |
| 268 | + | Export { | |
| 269 | + | path: std::path::PathBuf, | |
| 270 | + | opts: audiofiles_core::analysis::afcl::ExportOptions, | |
| 271 | + | }, | |
| 261 | 272 | /// k-NN tag suggestions for a single sample. Building the exemplar index is | |
| 262 | 273 | /// O(library), so the detail-panel "Suggest tags" action runs it here rather | |
| 263 | 274 | /// than under the DB lock on the GUI thread. | |
| @@ -273,7 +284,10 @@ pub enum ClassifierJobResult { | |||
| 273 | 284 | Exported(std::path::PathBuf), | |
| 274 | 285 | /// Suggestions for the sample identified by `hash`. The hash lets the GUI | |
| 275 | 286 | /// drop the result if the selection changed while the job ran. | |
| 276 | - | Suggested { hash: String, outcome: audiofiles_core::analysis::exemplar::MlOutcome }, | |
| 287 | + | Suggested { | |
| 288 | + | hash: String, | |
| 289 | + | outcome: audiofiles_core::analysis::exemplar::MlOutcome, | |
| 290 | + | }, | |
| 277 | 291 | } | |
| 278 | 292 | ||
| 279 | 293 | /// The core abstraction separating UI from data access. | |
| @@ -361,7 +375,6 @@ pub trait VfsBackend { | |||
| 361 | 375 | ||
| 362 | 376 | /// Sample tagging: per-sample and bulk tag mutation and lookup. | |
| 363 | 377 | pub trait TagBackend { | |
| 364 | - | ||
| 365 | 378 | // --- Tags --- | |
| 366 | 379 | ||
| 367 | 380 | /// Add a tag to a sample. | |
| @@ -394,7 +407,6 @@ pub trait TagBackend { | |||
| 394 | 407 | ||
| 395 | 408 | /// Text/filter search over the library. | |
| 396 | 409 | pub trait SearchBackend { | |
| 397 | - | ||
| 398 | 410 | // --- Search --- | |
| 399 | 411 | ||
| 400 | 412 | /// Search within a specific VFS folder. | |
| @@ -411,7 +423,6 @@ pub trait SearchBackend { | |||
| 411 | 423 | ||
| 412 | 424 | /// Collections and smart (dynamic) folders. | |
| 413 | 425 | pub trait CollectionBackend { | |
| 414 | - | ||
| 415 | 426 | // --- Smart folders --- | |
| 416 | 427 | ||
| 417 | 428 | // --- Collections --- | |
| @@ -420,10 +431,18 @@ pub trait CollectionBackend { | |||
| 420 | 431 | fn list_collections(&self) -> BackendResult<Vec<Collection>>; | |
| 421 | 432 | ||
| 422 | 433 | /// Create a manual collection. Returns the new ID. | |
| 423 | - | fn create_collection(&self, name: &str, description: Option<&str>) -> BackendResult<CollectionId>; | |
| 434 | + | fn create_collection( | |
| 435 | + | &self, | |
| 436 | + | name: &str, | |
| 437 | + | description: Option<&str>, | |
| 438 | + | ) -> BackendResult<CollectionId>; | |
| 424 | 439 | ||
| 425 | 440 | /// Create a dynamic collection (saved search). Returns the new ID. | |
| 426 | - | fn create_dynamic_collection(&self, name: &str, filter: &SearchFilter) -> BackendResult<CollectionId>; | |
| 441 | + | fn create_dynamic_collection( | |
| 442 | + | &self, | |
| 443 | + | name: &str, | |
| 444 | + | filter: &SearchFilter, | |
| 445 | + | ) -> BackendResult<CollectionId>; | |
| 427 | 446 | ||
| 428 | 447 | /// Rename a collection. | |
| 429 | 448 | fn rename_collection(&self, id: CollectionId, new_name: &str) -> BackendResult<()>; | |
| @@ -432,10 +451,18 @@ pub trait CollectionBackend { | |||
| 432 | 451 | fn delete_collection(&self, id: CollectionId) -> BackendResult<()>; | |
| 433 | 452 | ||
| 434 | 453 | /// Add a sample to a collection (no-op if already present). | |
| 435 | - | fn add_to_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()>; | |
| 454 | + | fn add_to_collection( | |
| 455 | + | &self, | |
| 456 | + | collection_id: CollectionId, | |
| 457 | + | sample_hash: &str, | |
| 458 | + | ) -> BackendResult<()>; | |
| 436 | 459 | ||
| 437 | 460 | /// Remove a sample from a collection. | |
| 438 | - | fn remove_from_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()>; | |
| 461 | + | fn remove_from_collection( | |
| 462 | + | &self, | |
| 463 | + | collection_id: CollectionId, | |
| 464 | + | sample_hash: &str, | |
| 465 | + | ) -> BackendResult<()>; | |
| 439 | 466 | ||
| 440 | 467 | /// List sample hashes in a collection. | |
| 441 | 468 | fn list_collection_members(&self, collection_id: CollectionId) -> BackendResult<Vec<String>>; | |
| @@ -446,7 +473,6 @@ pub trait CollectionBackend { | |||
| 446 | 473 | ||
| 447 | 474 | /// Feature analysis: read/persist results and rule application over them. | |
| 448 | 475 | pub trait AnalysisBackend { | |
| 449 | - | ||
| 450 | 476 | // --- Analysis --- | |
| 451 | 477 | ||
| 452 | 478 | /// Get the full analysis result for a sample, if it exists. | |
| @@ -473,7 +499,6 @@ pub trait AnalysisBackend { | |||
| 473 | 499 | ||
| 474 | 500 | /// Tag rules (classifier Layer A). | |
| 475 | 501 | pub trait RulesBackend { | |
| 476 | - | ||
| 477 | 502 | // --- Tag rules (classifier Layer A) --- | |
| 478 | 503 | ||
| 479 | 504 | /// All tag rules in evaluation (priority) order. | |
| @@ -510,7 +535,6 @@ pub trait RulesBackend { | |||
| 510 | 535 | ||
| 511 | 536 | /// Auto-tagging (Layer B): k-NN suggestions, thresholds, the trained head, .afcl sharing, clustering, and harvesting. | |
| 512 | 537 | pub trait ClassifierBackend { | |
| 513 | - | ||
| 514 | 538 | // --- Auto-tagging: k-NN suggestions, thresholds, bootstrap (Layer B) --- | |
| 515 | 539 | ||
| 516 | 540 | // Single-sample k-NN suggestions run on the classifier worker | |
| @@ -598,8 +622,7 @@ pub trait ClassifierBackend { | |||
| 598 | 622 | fn apply_cluster_tag(&self, members: &[String], tag: &str) -> BackendResult<usize>; | |
| 599 | 623 | ||
| 600 | 624 | /// Folder-derived tag candidates from the VFS structure. | |
| 601 | - | fn harvest_folder_labels(&self) | |
| 602 | - | -> BackendResult<Vec<audiofiles_core::harvest::FolderLabel>>; | |
| 625 | + | fn harvest_folder_labels(&self) -> BackendResult<Vec<audiofiles_core::harvest::FolderLabel>>; | |
| 603 | 626 | ||
| 604 | 627 | /// Apply a harvested tag to the given samples (`source = 'harvest'`). | |
| 605 | 628 | fn apply_harvested_tag(&self, hashes: &[String], tag: &str) -> BackendResult<usize>; | |
| @@ -613,7 +636,6 @@ pub trait ClassifierBackend { | |||
| 613 | 636 | ||
| 614 | 637 | /// Similarity and near-duplicate search. | |
| 615 | 638 | pub trait SimilarityBackend { | |
| 616 | - | ||
| 617 | 639 | // --- Similarity --- | |
| 618 | 640 | ||
| 619 | 641 | /// Start a similarity search for `hash`. The VP-tree build + query run on the | |
| @@ -628,7 +650,6 @@ pub trait SimilarityBackend { | |||
| 628 | 650 | ||
| 629 | 651 | /// Content-addressed store: import, sample paths, trash, and loose-file maintenance. | |
| 630 | 652 | pub trait StoreBackend { | |
| 631 | - | ||
| 632 | 653 | // --- Store --- | |
| 633 | 654 | ||
| 634 | 655 | /// Import a file into the content-addressed store. Returns the hash. | |
| @@ -719,7 +740,6 @@ pub trait StoreBackend { | |||
| 719 | 740 | ||
| 720 | 741 | /// Export item collection and device profiles. | |
| 721 | 742 | pub trait ExportBackend { | |
| 722 | - | ||
| 723 | 743 | // --- Export --- | |
| 724 | 744 | ||
| 725 | 745 | /// Collect export items from a VFS subtree. | |
| @@ -749,7 +769,6 @@ pub trait ExportBackend { | |||
| 749 | 769 | ||
| 750 | 770 | /// Sample Forge: chop and conform. | |
| 751 | 771 | pub trait ForgeBackend { | |
| 752 | - | ||
| 753 | 772 | // --- Sample Forge --- | |
| 754 | 773 | ||
| 755 | 774 | /// Start computing slice-boundary positions (normalized 0..1 fractions of | |
| @@ -814,7 +833,6 @@ pub trait ForgeBackend { | |||
| 814 | 833 | ||
| 815 | 834 | /// App config key/value and per-VFS sync toggles. | |
| 816 | 835 | pub trait ConfigBackend { | |
| 817 | - | ||
| 818 | 836 | // --- Config --- | |
| 819 | 837 | ||
| 820 | 838 | /// Get a user config value by key. | |
| @@ -835,7 +853,6 @@ pub trait ConfigBackend { | |||
| 835 | 853 | ||
| 836 | 854 | /// VFS mirror sync plus the long-running background workers (import/analysis/export/edit/cleanup), edit history, storage stats, and event polling. | |
| 837 | 855 | pub trait OperationsBackend { | |
| 838 | - | ||
| 839 | 856 | // --- VFS Mirror --- | |
| 840 | 857 | ||
| 841 | 858 | /// Synchronise the VFS mirror directory with the current VFS state. | |
| @@ -845,11 +862,7 @@ pub trait OperationsBackend { | |||
| 845 | 862 | // --- Long-running operations --- | |
| 846 | 863 | ||
| 847 | 864 | /// Start a folder import in the background. | |
| 848 | - | fn start_import( | |
| 849 | - | &self, | |
| 850 | - | source: &Path, | |
| 851 | - | strategy: ImportStrategyDesc, | |
| 852 | - | ) -> BackendResult<()>; | |
| 865 | + | fn start_import(&self, source: &Path, strategy: ImportStrategyDesc) -> BackendResult<()>; | |
| 853 | 866 | ||
| 854 | 867 | /// Start a background import of an explicit list of files (loose drops, CLI | |
| 855 | 868 | /// args, OS "Open with"). Runs on the same worker as [`start_import`] so the | |
| @@ -906,11 +919,7 @@ pub trait OperationsBackend { | |||
| 906 | 919 | /// Delete the most recent `edit_history` row matching this (source, result) | |
| 907 | 920 | /// pair. Used by `BrowserState::undo_last_edit` to reverse a previously | |
| 908 | 921 | /// recorded edit when the user clicks the inline Undo affordance. | |
| 909 | - | fn delete_edit_history( | |
| 910 | - | &self, | |
| 911 | - | source_hash: &str, | |
| 912 | - | result_hash: &str, | |
| 913 | - | ) -> BackendResult<()>; | |
| 922 | + | fn delete_edit_history(&self, source_hash: &str, result_hash: &str) -> BackendResult<()>; | |
| 914 | 923 | ||
| 915 | 924 | /// Get aggregate storage statistics for the current vault. | |
| 916 | 925 | fn storage_stats(&self) -> BackendResult<StorageStats>; | |
| @@ -975,11 +984,16 @@ mod tests { | |||
| 975 | 984 | ||
| 976 | 985 | #[test] | |
| 977 | 986 | fn import_strategy_desc_variants() { | |
| 978 | - | let flat = ImportStrategyDesc::Flat { vfs_id: VfsId::from(1), parent_id: None }; | |
| 987 | + | let flat = ImportStrategyDesc::Flat { | |
| 988 | + | vfs_id: VfsId::from(1), | |
| 989 | + | parent_id: None, | |
| 990 | + | }; | |
| 979 | 991 | let json = serde_json::to_string(&flat).unwrap(); | |
| 980 | 992 | assert!(json.contains("Flat")); | |
| 981 | 993 | ||
| 982 | - | let new_vfs = ImportStrategyDesc::NewVfs { vfs_name: "Test".to_string() }; | |
| 994 | + | let new_vfs = ImportStrategyDesc::NewVfs { | |
| 995 | + | vfs_name: "Test".to_string(), | |
| 996 | + | }; | |
| 983 | 997 | let json = serde_json::to_string(&new_vfs).unwrap(); | |
| 984 | 998 | assert!(json.contains("Test")); | |
| 985 | 999 | } |