diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 381340d..803af0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,4 +51,4 @@ jobs: # machine; CI runners have no CUDA toolkit and must build CPU-only. - name: Clippy working-directory: src-tauri - run: cargo clippy --all-targets --locked --no-default-features + run: cargo clippy --all-targets --locked --no-default-features -- -D warnings diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 7a66e37..b114a10 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -232,7 +232,7 @@ pub async fn add_folder( .asset_protocol_scope() .allow_directory(&folder_path, true) { - log::error!("Failed to allow asset scope for {}: {}", path, error); + log::error!("Failed to allow asset scope for {path}: {error}"); } let name = folder_path @@ -403,7 +403,7 @@ pub async fn update_folder_path( ) -> Result<(), String> { let new_path_buf = PathBuf::from(&new_path); if !new_path_buf.is_dir() { - return Err(format!("Path is not a valid directory: {}", new_path)); + return Err(format!("Path is not a valid directory: {new_path}")); } // Let the webview load media from the relocated folder via the asset protocol. @@ -411,7 +411,7 @@ pub async fn update_folder_path( .asset_protocol_scope() .allow_directory(&new_path_buf, true) { - log::error!("Failed to allow asset scope for {}: {}", new_path, error); + log::error!("Failed to allow asset scope for {new_path}: {error}"); } let new_name = new_path_buf @@ -593,7 +593,7 @@ pub async fn semantic_search_images( let has_filters = params.folder_id.is_some() || params.media_kind.is_some() || params.favorites_only.unwrap_or(false) - || params.rating_min.map_or(false, |r| r > 0); + || params.rating_min.is_some_and(|r| r > 0); if !has_filters { // No post-query filtering — a single fetch of exactly `limit` results is optimal. @@ -894,7 +894,7 @@ pub async fn get_tag_cloud( hasher.digest() }; let folder_scope = match folder_id { - Some(id) => format!("folder_{}", id), + Some(id) => format!("folder_{id}"), None => "all".to_string(), }; @@ -1038,132 +1038,131 @@ pub async fn find_duplicates( // For sample-hash groups that contain files > 64 KB, compute a full-file // hash to confirm the match before presenting them as deletable duplicates. let app_hash = app.clone(); - let (pairs, skipped_before_confirm, candidate_total): ( - Vec<(u64, u64, i64, String)>, - usize, - usize, - ) = tokio::task::spawn_blocking(move || { - use memmap2::Mmap; - use rayon::prelude::*; - use std::collections::HashMap; - use std::sync::atomic::{AtomicUsize, Ordering}; - use xxhash_rust::xxh3::{xxh3_64, Xxh3}; + // (size, sample/full hash, image_id, path) for each confirmed candidate. + type HashedCandidate = (u64, u64, i64, String); + let (pairs, skipped_before_confirm, candidate_total): (Vec, usize, usize) = + tokio::task::spawn_blocking(move || { + use memmap2::Mmap; + use rayon::prelude::*; + use std::collections::HashMap; + use std::sync::atomic::{AtomicUsize, Ordering}; + use xxhash_rust::xxh3::{xxh3_64, Xxh3}; - const WINDOW: usize = 16 * 1024; // 16 KB per sample window - const N: usize = 4; // windows at 0%, 33%, 66%, 100% - const COVERED: usize = WINDOW * N; // 64 KB total; also full-coverage threshold + const WINDOW: usize = 16 * 1024; // 16 KB per sample window + const N: usize = 4; // windows at 0%, 33%, 66%, 100% + const COVERED: usize = WINDOW * N; // 64 KB total; also full-coverage threshold - // Hash four evenly-spaced 16 KB windows. For files ≤ 64 KB this is - // equivalent to hashing the entire file. - fn sample(mmap: &[u8]) -> u64 { - if mmap.len() <= COVERED { - return xxh3_64(mmap); + // Hash four evenly-spaced 16 KB windows. For files ≤ 64 KB this is + // equivalent to hashing the entire file. + fn sample(mmap: &[u8]) -> u64 { + if mmap.len() <= COVERED { + return xxh3_64(mmap); + } + let mut h = Xxh3::new(); + for i in 0..N { + let pos = (mmap.len() - WINDOW) * i / (N - 1); + h.update(&mmap[pos..pos + WINDOW]); + } + h.digest() } - let mut h = Xxh3::new(); - for i in 0..N { - let pos = (mmap.len() - WINDOW) * i / (N - 1); - h.update(&mmap[pos..pos + WINDOW]); - } - h.digest() - } - // ── Phase 1: stat ───────────────────────────────────────────────── - let stat_counter = AtomicUsize::new(0); - let stat_skipped = AtomicUsize::new(0); - let sized: Vec<(i64, String, u64)> = records - .par_iter() - .filter_map(|r| { - let result = match std::fs::metadata(&r.path) { - Ok(metadata) => Some((r.id, r.path.clone(), metadata.len())), - Err(_) => { - stat_skipped.fetch_add(1, Ordering::Relaxed); - None + // ── Phase 1: stat ───────────────────────────────────────────────── + let stat_counter = AtomicUsize::new(0); + let stat_skipped = AtomicUsize::new(0); + let sized: Vec<(i64, String, u64)> = records + .par_iter() + .filter_map(|r| { + let result = match std::fs::metadata(&r.path) { + Ok(metadata) => Some((r.id, r.path.clone(), metadata.len())), + Err(_) => { + stat_skipped.fetch_add(1, Ordering::Relaxed); + None + } + }; + let done = stat_counter.fetch_add(1, Ordering::Relaxed) + 1; + if done % 100 == 0 || done == total { + let _ = app_hash.emit( + "duplicate_scan_progress", + DuplicateScanProgress { + phase: "checking".to_string(), + processed: done, + total, + skipped: stat_skipped.load(Ordering::Relaxed), + }, + ); } - }; - let done = stat_counter.fetch_add(1, Ordering::Relaxed) + 1; - if done % 100 == 0 || done == total { - let _ = app_hash.emit( - "duplicate_scan_progress", - DuplicateScanProgress { - phase: "checking".to_string(), - processed: done, - total, - skipped: stat_skipped.load(Ordering::Relaxed), - }, - ); - } - result - }) - .collect(); - let stat_skipped = stat_skipped.load(Ordering::Relaxed); + result + }) + .collect(); + let stat_skipped = stat_skipped.load(Ordering::Relaxed); - let mut by_size: HashMap> = HashMap::new(); - for (i, (_, _, size)) in sized.iter().enumerate() { - by_size.entry(*size).or_default().push(i); - } - let candidates: Vec = by_size - .into_values() - .filter(|g| g.len() > 1) - .flatten() - .collect(); + let mut by_size: HashMap> = HashMap::new(); + for (i, (_, _, size)) in sized.iter().enumerate() { + by_size.entry(*size).or_default().push(i); + } + let candidates: Vec = by_size + .into_values() + .filter(|g| g.len() > 1) + .flatten() + .collect(); - if candidates.is_empty() { - return (vec![], stat_skipped, 0); - } + if candidates.is_empty() { + return (vec![], stat_skipped, 0); + } - // ── Phase 2: sample hash ────────────────────────────────────────── - let c_total = candidates.len(); - let _ = app_hash.emit( - "duplicate_scan_progress", - DuplicateScanProgress { - phase: "hashing".to_string(), - processed: 0, - total: c_total, - skipped: stat_skipped, - }, - ); - let counter = AtomicUsize::new(0); - let hash_skipped = AtomicUsize::new(0); + // ── Phase 2: sample hash ────────────────────────────────────────── + let c_total = candidates.len(); + let _ = app_hash.emit( + "duplicate_scan_progress", + DuplicateScanProgress { + phase: "hashing".to_string(), + processed: 0, + total: c_total, + skipped: stat_skipped, + }, + ); + let counter = AtomicUsize::new(0); + let hash_skipped = AtomicUsize::new(0); - let pairs = candidates - .par_iter() - .filter_map(|&idx| { - let (id, path, size) = &sized[idx]; - let result = if *size == 0 { - Some((xxh3_64(&[]), *size, *id, path.clone())) - } else { - std::fs::File::open(path) - .ok() - // SAFETY: read-only; no external truncation expected during scan. - .and_then(|file| unsafe { Mmap::map(&file).ok() }) - .map(|mmap| (sample(&mmap), *size, *id, path.clone())) - }; - if result.is_none() { - hash_skipped.fetch_add(1, Ordering::Relaxed); - } - let done = counter.fetch_add(1, Ordering::Relaxed) + 1; - if done % 100 == 0 || done == c_total { - let _ = app_hash.emit( - "duplicate_scan_progress", - DuplicateScanProgress { - phase: "hashing".to_string(), - processed: done, - total: c_total, - skipped: stat_skipped + hash_skipped.load(Ordering::Relaxed), - }, - ); - } - result - }) - .collect(); - ( - pairs, - stat_skipped + hash_skipped.load(Ordering::Relaxed), - c_total, - ) - }) - .await - .map_err(|e| e.to_string())?; + let pairs = candidates + .par_iter() + .filter_map(|&idx| { + let (id, path, size) = &sized[idx]; + let result = if *size == 0 { + Some((xxh3_64(&[]), *size, *id, path.clone())) + } else { + std::fs::File::open(path) + .ok() + // SAFETY: read-only; no external truncation expected during scan. + .and_then(|file| unsafe { Mmap::map(&file).ok() }) + .map(|mmap| (sample(&mmap), *size, *id, path.clone())) + }; + if result.is_none() { + hash_skipped.fetch_add(1, Ordering::Relaxed); + } + let done = counter.fetch_add(1, Ordering::Relaxed) + 1; + if done % 100 == 0 || done == c_total { + let _ = app_hash.emit( + "duplicate_scan_progress", + DuplicateScanProgress { + phase: "hashing".to_string(), + processed: done, + total: c_total, + skipped: stat_skipped + hash_skipped.load(Ordering::Relaxed), + }, + ); + } + result + }) + .collect(); + ( + pairs, + stat_skipped + hash_skipped.load(Ordering::Relaxed), + c_total, + ) + }) + .await + .map_err(|e| e.to_string())?; // Group by (sample_hash, file_size). let mut size_hash_map: std::collections::HashMap<(u64, u64), Vec<(i64, String)>> = @@ -1276,7 +1275,7 @@ pub async fn find_duplicates( .filter_map(|(hash, file_size, ids)| { let images = db::get_images_by_ids(&conn, &ids).ok()?; Some(DuplicateGroup { - file_hash: format!("{:016x}", hash), + file_hash: format!("{hash:016x}"), file_size, images, }) @@ -1288,7 +1287,7 @@ pub async fn find_duplicates( // Persist results so they survive restart — best-effort, ignore errors. let folder_scope = match folder_id { - Some(id) => format!("folder:{}", id), + Some(id) => format!("folder:{id}"), None => "all".to_string(), }; if let Ok(json) = serde_json::to_string(&groups) { @@ -1309,7 +1308,7 @@ pub async fn load_duplicate_scan_cache( folder_id: Option, ) -> Result, String> { let folder_scope = match folder_id { - Some(id) => format!("folder:{}", id), + Some(id) => format!("folder:{id}"), None => "all".to_string(), }; let conn = db.get().map_err(|e| e.to_string())?; @@ -1329,7 +1328,7 @@ pub async fn invalidate_duplicate_scan_cache( folder_id: Option, ) -> Result<(), String> { let folder_scope = match folder_id { - Some(id) => format!("folder:{}", id), + Some(id) => format!("folder:{id}"), None => "all".to_string(), }; let conn = db.get().map_err(|e| e.to_string())?; @@ -1382,7 +1381,7 @@ fn dot(a: &[f32], b: &[f32]) -> f32 { a.iter().zip(b.iter()).map(|(x, y)| x * y).sum() } -fn normalize(v: &mut Vec) { +fn normalize(v: &mut [f32]) { let norm: f32 = v.iter().map(|x| x * x).sum::().sqrt(); if norm > 1e-10 { v.iter_mut().for_each(|x| *x /= norm); diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 51f5c57..7596055 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -99,6 +99,8 @@ pub struct MetadataJob { pub path: String, } +// Caption worker disabled (lib.rs) — kept for future re-enabling. +#[allow(dead_code)] #[derive(Debug, Clone)] pub struct CaptionJob { pub image_id: i64, @@ -588,6 +590,7 @@ pub fn enqueue_caption_job(conn: &Connection, image_id: i64) -> Result<()> { Ok(()) } +#[allow(dead_code)] // caption worker disabled (lib.rs) pub fn requeue_caption_jobs(conn: &Connection, image_ids: &[i64]) -> Result<()> { for image_id in image_ids { conn.execute( @@ -666,6 +669,7 @@ pub fn clear_caption_jobs(conn: &Connection, folder_id: Option) -> Result Result { let count: i64 = conn.query_row( "SELECT COUNT(*) FROM caption_jobs WHERE image_id = ?1 AND status = 'cancelled'", @@ -863,6 +867,7 @@ pub fn claim_embedding_jobs( Ok(claimed) } +#[allow(dead_code)] // caption worker disabled (lib.rs) fn get_pending_caption_jobs_excluding( conn: &Connection, excluded_folder_ids: &std::collections::HashSet, @@ -891,6 +896,7 @@ fn get_pending_caption_jobs_excluding( Ok(rows.collect::>>()?) } +#[allow(dead_code)] // caption worker disabled (lib.rs) pub fn claim_caption_jobs( conn: &mut Connection, paused_folder_ids: &std::collections::HashSet, @@ -1434,6 +1440,7 @@ pub fn get_indexed_entry_by_path( /// Look up just the image id for a path. Used by the filesystem watcher /// to find the DB row to delete when a file is removed from disk. +#[allow(dead_code)] // only caller is the disabled caption worker path pub fn get_image_id_by_path(conn: &Connection, path: &str) -> Result> { let result = conn.query_row("SELECT id FROM images WHERE path = ?1", [path], |row| { row.get(0) @@ -1539,6 +1546,7 @@ pub fn clear_folder_scan_error(conn: &Connection, folder_id: i64) -> Result<()> Ok(()) } +#[allow(clippy::too_many_arguments)] // mirrors the gallery query surface; a params struct adds noise for one caller pub fn get_images( conn: &Connection, folder_id: Option, @@ -1567,7 +1575,7 @@ pub fn get_images( _ => "modified_at DESC NULLS LAST", }; - let search_pattern = search.map(|value| format!("%{}%", value)); + let search_pattern = search.map(|value| format!("%{value}%")); let favorites_flag = i64::from(favorites_only); let embedding_failed_flag = i64::from(embedding_failed_only); let sql = format!( @@ -1583,9 +1591,8 @@ pub fn get_images( AND (?4 = 0 OR favorite = 1) AND rating >= ?5 AND (?6 = 0 OR embedding_status = 'failed') - ORDER BY {} - LIMIT ?7 OFFSET ?8", - order + ORDER BY {order} + LIMIT ?7 OFFSET ?8" ); let mut stmt = conn.prepare(&sql)?; let rows = stmt.query_map( @@ -1613,7 +1620,7 @@ pub fn count_images( rating_min: i64, embedding_failed_only: bool, ) -> Result { - let search_pattern = search.map(|value| format!("%{}%", value)); + let search_pattern = search.map(|value| format!("%{value}%")); let favorites_flag = i64::from(favorites_only); let embedding_failed_flag = i64::from(embedding_failed_only); @@ -1639,6 +1646,7 @@ pub fn count_images( Ok(count) } +#[allow(clippy::too_many_arguments)] pub fn search_images_by_tag( conn: &Connection, query: &str, @@ -2388,7 +2396,7 @@ pub fn set_duplicate_scan_cache( } fn ensure_column(conn: &Connection, table: &str, column: &str, definition: &str) -> Result<()> { - let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", table))?; + let mut stmt = conn.prepare(&format!("PRAGMA table_info({table})"))?; let mut rows = stmt.query([])?; while let Some(row) = rows.next()? { let existing_name: String = row.get(1)?; @@ -2398,7 +2406,7 @@ fn ensure_column(conn: &Connection, table: &str, column: &str, definition: &str) } conn.execute( - &format!("ALTER TABLE {} ADD COLUMN {} {}", table, column, definition), + &format!("ALTER TABLE {table} ADD COLUMN {column} {definition}"), [], )?; Ok(()) @@ -2425,5 +2433,5 @@ fn folder_exclusion_clause( .map(|id| id.to_string()) .collect::>() .join(","); - format!("AND {}.folder_id NOT IN ({})", image_alias, id_list) + format!("AND {image_alias}.folder_id NOT IN ({id_list})") } diff --git a/src-tauri/src/embedder.rs b/src-tauri/src/embedder.rs index 7c0b262..7f82ce2 100644 --- a/src-tauri/src/embedder.rs +++ b/src-tauri/src/embedder.rs @@ -160,7 +160,7 @@ impl ClipImageEmbedder { let ids = enc.get_ids(); let len = ids.len().min(max_len); for j in 0..len { - flat[i * max_len + j] = ids[j] as u32; + flat[i * max_len + j] = ids[j]; } } diff --git a/src-tauri/src/indexer.rs b/src-tauri/src/indexer.rs index a0d7d1d..c4e922f 100644 --- a/src-tauri/src/indexer.rs +++ b/src-tauri/src/indexer.rs @@ -24,6 +24,7 @@ const IMAGE_EXTENSIONS: &[&str] = &[ const VIDEO_EXTENSIONS: &[&str] = &["mp4", "mov", "m4v", "webm"]; const JOB_PROGRESS_EMIT_INTERVAL: Duration = Duration::from_millis(750); +#[allow(dead_code)] // caption worker disabled (lib.rs) const CAPTION_BATCH_SIZE: usize = 1; static LAST_JOB_PROGRESS_EMIT: OnceLock>> = OnceLock::new(); @@ -200,7 +201,7 @@ pub fn index_folder(app: AppHandle, pool: DbPool, folder_id: i64, folder_path: P // not be silently destroyed. if !folder_path.is_dir() { let error_msg = format!("Folder not found: {}", folder_path.display()); - log::error!("Indexing error for folder {}: {}", folder_id, error_msg); + log::error!("Indexing error for folder {folder_id}: {error_msg}"); if let Ok(conn) = pool.get() { let _ = db::set_folder_scan_error(&conn, folder_id, &error_msg); } @@ -221,7 +222,7 @@ pub fn index_folder(app: AppHandle, pool: DbPool, folder_id: i64, folder_path: P let storage_profile = detect_storage_profile(&folder_path); set_folder_storage_profile(folder_id, RuntimeAdaptiveProfile::new(storage_profile)); if let Err(error) = do_index(app.clone(), &pool, folder_id, folder_path) { - log::error!("Indexing error for folder {}: {}", folder_id, error); + log::error!("Indexing error for folder {folder_id}: {error}"); if let Ok(conn) = pool.get() { let _ = db::set_folder_scan_error(&conn, folder_id, &error.to_string()); } @@ -254,7 +255,7 @@ pub fn start_thumbnail_worker( Ok(true) => {} Ok(false) => std::thread::sleep(std::time::Duration::from_millis(250)), Err(error) => { - log::error!("Thumbnail worker error: {}", error); + log::error!("Thumbnail worker error: {error}"); std::thread::sleep(std::time::Duration::from_millis(250)); } } @@ -269,7 +270,7 @@ pub fn start_metadata_worker(app: AppHandle, pool: DbPool, media_tools: MediaToo Ok(true) => {} Ok(false) => std::thread::sleep(std::time::Duration::from_millis(250)), Err(error) => { - log::error!("Metadata worker error: {}", error); + log::error!("Metadata worker error: {error}"); std::thread::sleep(std::time::Duration::from_millis(250)); } } @@ -287,7 +288,7 @@ pub fn start_embedding_worker(app: AppHandle, pool: DbPool) { Ok(true) => {} Ok(false) => std::thread::sleep(std::time::Duration::from_millis(500)), Err(error) => { - log::error!("Embedding worker error: {}", error); + log::error!("Embedding worker error: {error}"); std::thread::sleep(std::time::Duration::from_millis(500)); } } @@ -295,6 +296,7 @@ pub fn start_embedding_worker(app: AppHandle, pool: DbPool) { }); } +#[allow(dead_code)] // caption worker disabled (lib.rs) pub fn start_caption_worker(app: AppHandle, pool: DbPool, app_data_dir: PathBuf) { std::thread::spawn(move || { let mut captioner: Option = None; @@ -307,7 +309,7 @@ pub fn start_caption_worker(app: AppHandle, pool: DbPool, app_data_dir: PathBuf) captioner = None; } if let Err(error) = process_caption_batch(&app, &pool, &app_data_dir, &mut captioner) { - log::error!("Caption worker error: {}", error); + log::error!("Caption worker error: {error}"); captioner = None; } std::thread::sleep(std::time::Duration::from_millis(750)); @@ -332,7 +334,7 @@ pub fn start_tagging_worker(app: AppHandle, pool: DbPool, app_data_dir: PathBuf) Ok(true) => {} Ok(false) => std::thread::sleep(std::time::Duration::from_millis(750)), Err(error) => { - log::error!("Tagging worker error: {}", error); + log::error!("Tagging worker error: {error}"); tagger_instance = None; std::thread::sleep(std::time::Duration::from_millis(750)); } @@ -414,7 +416,7 @@ fn do_index(app: AppHandle, pool: &DbPool, folder_id: i64, folder_path: PathBuf) } if !records.is_empty() { - let committed = commit_batch(&pool, &records)?; + let committed = commit_batch(pool, &records)?; emit_images( &app, &IndexedImagesBatch { @@ -422,7 +424,7 @@ fn do_index(app: AppHandle, pool: &DbPool, folder_id: i64, folder_path: PathBuf) images: committed, }, ); - emit_folder_job_progress(&app, &pool, &[folder_id], false); + emit_folder_job_progress(&app, pool, &[folder_id], false); } processed += path_chunk.len(); @@ -473,7 +475,7 @@ fn do_index(app: AppHandle, pool: &DbPool, folder_id: i64, folder_path: PathBuf) let _ = db::clear_folder_scan_error(&conn, folder_id); // Invalidate duplicate scan cache — any reindex can change file contents // or the set of files, making cached duplicate groups stale. - let folder_scope = format!("folder:{}", folder_id); + let folder_scope = format!("folder:{folder_id}"); let _ = db::clear_duplicate_scan_cache(&conn, &folder_scope); let _ = db::clear_duplicate_scan_cache(&conn, "all"); } @@ -488,7 +490,7 @@ fn do_index(app: AppHandle, pool: &DbPool, folder_id: i64, folder_path: PathBuf) done: true, }, ); - emit_folder_job_progress(&app, &pool, &[folder_id], true); + emit_folder_job_progress(&app, pool, &[folder_id], true); Ok(()) } @@ -897,10 +899,7 @@ fn process_embedding_batch( } } Err(batch_error) => { - log::error!( - "Embedding batch fallback to per-image mode: {}", - batch_error - ); + log::error!("Embedding batch fallback to per-image mode: {batch_error}"); for (job, source_path) in embeddable_jobs .into_iter() .zip(embeddable_paths.into_iter()) @@ -921,7 +920,7 @@ fn process_embedding_batch( for job in &jobs { let embedding_result: Result> = if let Some(err) = pre_failed.remove(&job.image_id) { - Err(anyhow::anyhow!("{}", err)) + Err(anyhow::anyhow!("{err}")) } else if let Some(r) = embed_results.remove(&job.image_id) { r } else { @@ -963,17 +962,13 @@ fn process_embedding_batch( let write_elapsed = write_started_at.elapsed(); let batch_elapsed = batch_started_at.elapsed(); log::info!( - "Embedding batch timing: claimed {} in {:?}, infer {:?}, write {:?}, total {:?}", - EMBEDDING_BATCH_SIZE, - claim_elapsed, - infer_elapsed, - write_elapsed, - batch_elapsed + "Embedding batch timing: claimed {EMBEDDING_BATCH_SIZE} in {claim_elapsed:?}, infer {infer_elapsed:?}, write {write_elapsed:?}, total {batch_elapsed:?}" ); Ok(true) } +#[allow(dead_code)] // caption worker disabled (lib.rs) fn process_caption_batch( app: &AppHandle, pool: &DbPool, @@ -1186,7 +1181,7 @@ fn process_tagging_batch( tx.commit()?; Ok(updated_images) }) - .or_else(|db_err| { + .inspect_err(|_db_err| { // The DB write failed. Try to requeue the claimed jobs so they aren't // left stuck in 'processing' until the next app restart. let image_ids: Vec = jobs.iter().map(|job| job.image_id).collect(); @@ -1194,7 +1189,6 @@ fn process_tagging_batch( let conn = pool.get()?; db::requeue_tagging_jobs(&conn, &image_ids) }); - Err(db_err) })?; if !updated_images.is_empty() { @@ -1300,7 +1294,7 @@ fn emit_media_updates(app: &AppHandle, batch: &MediaUpdateBatch) { } pub fn emit_folder_job_progress(app: &AppHandle, pool: &DbPool, folder_ids: &[i64], force: bool) { - let mut unique_folder_ids = folder_ids.iter().copied().collect::>(); + let mut unique_folder_ids = folder_ids.to_vec(); unique_folder_ids.sort_unstable(); unique_folder_ids.dedup(); @@ -1401,7 +1395,7 @@ impl WatcherHandle { let mut w = self.inner.watcher.lock().unwrap(); if path.is_dir() { if let Err(e) = w.watch(&path, RecursiveMode::Recursive) { - log::error!("Watcher: failed to watch {:?}: {}", path, e); + log::error!("Watcher: failed to watch {path:?}: {e}"); } } } @@ -1426,7 +1420,7 @@ impl WatcherHandle { let _ = w.unwatch(old_path); if new_path.is_dir() { if let Err(e) = w.watch(&new_path, RecursiveMode::Recursive) { - log::error!("Watcher: failed to watch {:?}: {}", new_path, e); + log::error!("Watcher: failed to watch {new_path:?}: {e}"); } } } @@ -1471,7 +1465,7 @@ pub fn start_watcher(app: AppHandle, pool: DbPool, thumb_dir: PathBuf) -> Watche for path in map.keys() { if path.is_dir() { if let Err(e) = w.watch(path, RecursiveMode::Recursive) { - log::error!("Watcher: failed to watch {:?}: {}", path, e); + log::error!("Watcher: failed to watch {path:?}: {e}"); } } } @@ -1597,7 +1591,7 @@ fn process_watcher_path( let conn = match pool.get() { Ok(c) => c, Err(e) => { - log::error!("Watcher: DB pool error: {}", e); + log::error!("Watcher: DB pool error: {e}"); return; } }; @@ -1632,7 +1626,7 @@ fn process_watcher_path( } } Ok(_) => {} - Err(e) => log::error!("Watcher: commit error for {:?}: {}", path, e), + Err(e) => log::error!("Watcher: commit error for {path:?}: {e}"), } } else { // File removed from disk — clean up DB row and thumbnail. @@ -1649,7 +1643,7 @@ fn process_watcher_path( } } Ok(None) => {} // never indexed or already removed - Err(e) => log::error!("Watcher: lookup error for {:?}: {}", path, e), + Err(e) => log::error!("Watcher: lookup error for {path:?}: {e}"), } } } @@ -1679,7 +1673,7 @@ fn process_watcher_rename( let conn = match pool.get() { Ok(c) => c, Err(e) => { - log::error!("Watcher rename: DB pool error: {}", e); + log::error!("Watcher rename: DB pool error: {e}"); return; } }; @@ -1696,7 +1690,7 @@ fn process_watcher_rename( return; } Err(e) => { - log::error!("Watcher rename: DB lookup error: {}", e); + log::error!("Watcher rename: DB lookup error: {e}"); return; } }; @@ -1721,7 +1715,7 @@ fn process_watcher_rename( new_filename, effective_thumb, ) { - log::error!("Watcher rename: DB update error: {}", e); + log::error!("Watcher rename: DB update error: {e}"); return; } @@ -1734,6 +1728,6 @@ fn process_watcher_rename( images: vec![record], }, ), - Err(e) => log::error!("Watcher rename: post-update fetch error: {}", e), + Err(e) => log::error!("Watcher rename: post-update fetch error: {e}"), } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 46b514c..882c9b7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -65,14 +65,13 @@ pub fn run() { let backfilled = db::backfill_embedding_jobs(&conn).expect("Failed to backfill embedding jobs"); if backfilled > 0 { - log::info!("Backfilled {} embedding jobs.", backfilled); + log::info!("Backfilled {backfilled} embedding jobs."); } let (orphaned_vectors, missing_vectors) = db::repair_embedding_consistency(&conn) .expect("Failed to repair embedding consistency"); if orphaned_vectors > 0 || missing_vectors > 0 { log::info!( - "Repaired embedding consistency: removed {} orphaned vectors, requeued {} missing vectors.", - orphaned_vectors, missing_vectors + "Repaired embedding consistency: removed {orphaned_vectors} orphaned vectors, requeued {missing_vectors} missing vectors." ); } } diff --git a/src-tauri/src/media.rs b/src-tauri/src/media.rs index a409eca..be64af5 100644 --- a/src-tauri/src/media.rs +++ b/src-tauri/src/media.rs @@ -49,11 +49,7 @@ impl MediaTools { total_bytes, downloaded_bytes, } => { - log::info!( - "Downloading bundled FFmpeg: {}/{} bytes", - downloaded_bytes, - total_bytes - ); + log::info!("Downloading bundled FFmpeg: {downloaded_bytes}/{total_bytes} bytes"); } FfmpegDownloadProgressEvent::UnpackingArchive => { log::info!("Unpacking bundled FFmpeg..."); diff --git a/src-tauri/src/storage.rs b/src-tauri/src/storage.rs index b43f80e..bf7f29e 100644 --- a/src-tauri/src/storage.rs +++ b/src-tauri/src/storage.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::Path; use sysinfo::{DiskKind, Disks}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -107,7 +107,7 @@ pub fn detect_storage_profile(path: &Path) -> StorageProfile { } } -fn fallback_profile_for_path(path: &PathBuf) -> StorageProfile { +fn fallback_profile_for_path(path: &Path) -> StorageProfile { let path_str = path.to_string_lossy().to_lowercase(); if path_str.starts_with("\\\\") { return StorageProfile::Conservative; diff --git a/src-tauri/src/tagger.rs b/src-tauri/src/tagger.rs index 47c5e4b..078a629 100644 --- a/src-tauri/src/tagger.rs +++ b/src-tauri/src/tagger.rs @@ -481,7 +481,7 @@ impl WdTagger { .try_extract_tensor::() .map_err(|error| anyhow::anyhow!("{error}"))?; - let probs: &[f32] = &probabilities; + let probs: &[f32] = probabilities; if probs.len() != self.labels.len() { anyhow::bail!( diff --git a/src-tauri/src/thumbnail.rs b/src-tauri/src/thumbnail.rs index 19bde01..fd5112c 100644 --- a/src-tauri/src/thumbnail.rs +++ b/src-tauri/src/thumbnail.rs @@ -237,9 +237,7 @@ pub fn generate_video_thumbnail( } Err(anyhow!( - "ffmpeg failed generating poster for {}: {}", - path_str, - last_error + "ffmpeg failed generating poster for {path_str}: {last_error}" )) } @@ -253,13 +251,27 @@ pub fn video_poster_path(cache_dir: &Path, video_path: &str) -> PathBuf { fn thumb_path_with_ext(cache_dir: &Path, input_path: &str, extension: &str) -> PathBuf { let hash = hash_path(input_path); - cache_dir.join(format!("{}.{}", hash, extension)) + cache_dir.join(format!("{hash}.{extension}")) } fn hash_path(s: &str) -> String { format!("{:016x}", xxhash_rust::xxh3::xxh3_64(s.as_bytes())) } +fn fit_dimensions(width: u32, height: u32, max_size: u32) -> (u32, u32) { + if width <= max_size && height <= max_size { + return (width.max(1), height.max(1)); + } + + if width >= height { + let scaled_height = ((height as f64 / width as f64) * max_size as f64).round() as u32; + (max_size, scaled_height.max(1)) + } else { + let scaled_width = ((width as f64 / height as f64) * max_size as f64).round() as u32; + (scaled_width.max(1), max_size) + } +} + #[cfg(test)] mod tests { use super::*; @@ -302,17 +314,3 @@ mod tests { assert_eq!((thumb_w, thumb_h), (320, 240)); } } - -fn fit_dimensions(width: u32, height: u32, max_size: u32) -> (u32, u32) { - if width <= max_size && height <= max_size { - return (width.max(1), height.max(1)); - } - - if width >= height { - let scaled_height = ((height as f64 / width as f64) * max_size as f64).round() as u32; - (max_size, scaled_height.max(1)) - } else { - let scaled_width = ((width as f64 / height as f64) * max_size as f64).round() as u32; - (scaled_width.max(1), max_size) - } -} diff --git a/src-tauri/src/vector.rs b/src-tauri/src/vector.rs index 98b5349..c32d22c 100644 --- a/src-tauri/src/vector.rs +++ b/src-tauri/src/vector.rs @@ -10,7 +10,14 @@ static SQLITE_VEC_INIT: Once = Once::new(); pub fn register_sqlite_vec() { SQLITE_VEC_INIT.call_once(|| unsafe { - sqlite3_auto_extension(Some(std::mem::transmute(sqlite3_vec_init as *const ()))); + sqlite3_auto_extension(Some(std::mem::transmute::< + *const (), + unsafe extern "C" fn( + *mut rusqlite::ffi::sqlite3, + *mut *mut std::os::raw::c_char, + *const rusqlite::ffi::sqlite3_api_routines, + ) -> i32, + >(sqlite3_vec_init as *const ()))); }); } @@ -18,14 +25,13 @@ pub fn migrate(conn: &Connection) -> Result<()> { conn.execute_batch(&format!( "CREATE VIRTUAL TABLE IF NOT EXISTS image_vec USING vec0( image_id INTEGER PRIMARY KEY, - embedding FLOAT[{}] distance_metric=cosine + embedding FLOAT[{CLIP_VECTOR_DIM}] distance_metric=cosine ); CREATE VIRTUAL TABLE IF NOT EXISTS caption_vec USING vec0( image_id INTEGER PRIMARY KEY, - embedding FLOAT[{}] distance_metric=cosine - );", - CLIP_VECTOR_DIM, CLIP_VECTOR_DIM + embedding FLOAT[{CLIP_VECTOR_DIM}] distance_metric=cosine + );" ))?; Ok(()) } @@ -465,7 +471,7 @@ pub fn has_image_vector(conn: &Connection, image_id: i64) -> Result { #[allow(dead_code)] fn pack_f32(values: &[f32]) -> Vec { - let mut out = Vec::with_capacity(values.len() * std::mem::size_of::()); + let mut out = Vec::with_capacity(std::mem::size_of_val(values)); for value in values { out.extend_from_slice(&value.to_le_bytes()); }