refactor(backend): zero clippy warnings, gate CI with -D warnings

Auto-fix pass (uninlined format args, needless borrows, to_vec,
size_of_val, map_err->inspect_err) plus hand fixes: fully annotated the
sqlite-vec transmute (c_char for cross-platform correctness), type alias
for the duplicate-scan tuple, slice signatures over &mut Vec/&PathBuf,
and #[allow(dead_code)] documenting the intentionally dormant caption
worker. The CI clippy step now fails on any new warning.
This commit is contained in:
2026-06-12 20:59:40 +01:00
parent 178754f6c3
commit 1640e30330
11 changed files with 207 additions and 207 deletions
+1 -1
View File
@@ -51,4 +51,4 @@ jobs:
# machine; CI runners have no CUDA toolkit and must build CPU-only. # machine; CI runners have no CUDA toolkit and must build CPU-only.
- name: Clippy - name: Clippy
working-directory: src-tauri working-directory: src-tauri
run: cargo clippy --all-targets --locked --no-default-features run: cargo clippy --all-targets --locked --no-default-features -- -D warnings
+14 -15
View File
@@ -232,7 +232,7 @@ pub async fn add_folder(
.asset_protocol_scope() .asset_protocol_scope()
.allow_directory(&folder_path, true) .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 let name = folder_path
@@ -403,7 +403,7 @@ pub async fn update_folder_path(
) -> Result<(), String> { ) -> Result<(), String> {
let new_path_buf = PathBuf::from(&new_path); let new_path_buf = PathBuf::from(&new_path);
if !new_path_buf.is_dir() { 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. // 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() .asset_protocol_scope()
.allow_directory(&new_path_buf, true) .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 let new_name = new_path_buf
@@ -593,7 +593,7 @@ pub async fn semantic_search_images(
let has_filters = params.folder_id.is_some() let has_filters = params.folder_id.is_some()
|| params.media_kind.is_some() || params.media_kind.is_some()
|| params.favorites_only.unwrap_or(false) || 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 { if !has_filters {
// No post-query filtering — a single fetch of exactly `limit` results is optimal. // 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() hasher.digest()
}; };
let folder_scope = match folder_id { let folder_scope = match folder_id {
Some(id) => format!("folder_{}", id), Some(id) => format!("folder_{id}"),
None => "all".to_string(), None => "all".to_string(),
}; };
@@ -1038,11 +1038,10 @@ pub async fn find_duplicates(
// For sample-hash groups that contain files > 64 KB, compute a full-file // For sample-hash groups that contain files > 64 KB, compute a full-file
// hash to confirm the match before presenting them as deletable duplicates. // hash to confirm the match before presenting them as deletable duplicates.
let app_hash = app.clone(); let app_hash = app.clone();
let (pairs, skipped_before_confirm, candidate_total): ( // (size, sample/full hash, image_id, path) for each confirmed candidate.
Vec<(u64, u64, i64, String)>, type HashedCandidate = (u64, u64, i64, String);
usize, let (pairs, skipped_before_confirm, candidate_total): (Vec<HashedCandidate>, usize, usize) =
usize, tokio::task::spawn_blocking(move || {
) = tokio::task::spawn_blocking(move || {
use memmap2::Mmap; use memmap2::Mmap;
use rayon::prelude::*; use rayon::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
@@ -1276,7 +1275,7 @@ pub async fn find_duplicates(
.filter_map(|(hash, file_size, ids)| { .filter_map(|(hash, file_size, ids)| {
let images = db::get_images_by_ids(&conn, &ids).ok()?; let images = db::get_images_by_ids(&conn, &ids).ok()?;
Some(DuplicateGroup { Some(DuplicateGroup {
file_hash: format!("{:016x}", hash), file_hash: format!("{hash:016x}"),
file_size, file_size,
images, images,
}) })
@@ -1288,7 +1287,7 @@ pub async fn find_duplicates(
// Persist results so they survive restart — best-effort, ignore errors. // Persist results so they survive restart — best-effort, ignore errors.
let folder_scope = match folder_id { let folder_scope = match folder_id {
Some(id) => format!("folder:{}", id), Some(id) => format!("folder:{id}"),
None => "all".to_string(), None => "all".to_string(),
}; };
if let Ok(json) = serde_json::to_string(&groups) { if let Ok(json) = serde_json::to_string(&groups) {
@@ -1309,7 +1308,7 @@ pub async fn load_duplicate_scan_cache(
folder_id: Option<i64>, folder_id: Option<i64>,
) -> Result<Option<DuplicateScanCache>, String> { ) -> Result<Option<DuplicateScanCache>, String> {
let folder_scope = match folder_id { let folder_scope = match folder_id {
Some(id) => format!("folder:{}", id), Some(id) => format!("folder:{id}"),
None => "all".to_string(), None => "all".to_string(),
}; };
let conn = db.get().map_err(|e| e.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<i64>, folder_id: Option<i64>,
) -> Result<(), String> { ) -> Result<(), String> {
let folder_scope = match folder_id { let folder_scope = match folder_id {
Some(id) => format!("folder:{}", id), Some(id) => format!("folder:{id}"),
None => "all".to_string(), None => "all".to_string(),
}; };
let conn = db.get().map_err(|e| e.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() a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
} }
fn normalize(v: &mut Vec<f32>) { fn normalize(v: &mut [f32]) {
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt(); let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 1e-10 { if norm > 1e-10 {
v.iter_mut().for_each(|x| *x /= norm); v.iter_mut().for_each(|x| *x /= norm);
+16 -8
View File
@@ -99,6 +99,8 @@ pub struct MetadataJob {
pub path: String, pub path: String,
} }
// Caption worker disabled (lib.rs) — kept for future re-enabling.
#[allow(dead_code)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CaptionJob { pub struct CaptionJob {
pub image_id: i64, pub image_id: i64,
@@ -588,6 +590,7 @@ pub fn enqueue_caption_job(conn: &Connection, image_id: i64) -> Result<()> {
Ok(()) Ok(())
} }
#[allow(dead_code)] // caption worker disabled (lib.rs)
pub fn requeue_caption_jobs(conn: &Connection, image_ids: &[i64]) -> Result<()> { pub fn requeue_caption_jobs(conn: &Connection, image_ids: &[i64]) -> Result<()> {
for image_id in image_ids { for image_id in image_ids {
conn.execute( conn.execute(
@@ -666,6 +669,7 @@ pub fn clear_caption_jobs(conn: &Connection, folder_id: Option<i64>) -> Result<u
} }
} }
#[allow(dead_code)] // caption worker disabled (lib.rs)
pub fn is_caption_job_cancelled(conn: &Connection, image_id: i64) -> Result<bool> { pub fn is_caption_job_cancelled(conn: &Connection, image_id: i64) -> Result<bool> {
let count: i64 = conn.query_row( let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM caption_jobs WHERE image_id = ?1 AND status = 'cancelled'", "SELECT COUNT(*) FROM caption_jobs WHERE image_id = ?1 AND status = 'cancelled'",
@@ -863,6 +867,7 @@ pub fn claim_embedding_jobs(
Ok(claimed) Ok(claimed)
} }
#[allow(dead_code)] // caption worker disabled (lib.rs)
fn get_pending_caption_jobs_excluding( fn get_pending_caption_jobs_excluding(
conn: &Connection, conn: &Connection,
excluded_folder_ids: &std::collections::HashSet<i64>, excluded_folder_ids: &std::collections::HashSet<i64>,
@@ -891,6 +896,7 @@ fn get_pending_caption_jobs_excluding(
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
} }
#[allow(dead_code)] // caption worker disabled (lib.rs)
pub fn claim_caption_jobs( pub fn claim_caption_jobs(
conn: &mut Connection, conn: &mut Connection,
paused_folder_ids: &std::collections::HashSet<i64>, paused_folder_ids: &std::collections::HashSet<i64>,
@@ -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 /// 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. /// 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<Option<i64>> { pub fn get_image_id_by_path(conn: &Connection, path: &str) -> Result<Option<i64>> {
let result = conn.query_row("SELECT id FROM images WHERE path = ?1", [path], |row| { let result = conn.query_row("SELECT id FROM images WHERE path = ?1", [path], |row| {
row.get(0) row.get(0)
@@ -1539,6 +1546,7 @@ pub fn clear_folder_scan_error(conn: &Connection, folder_id: i64) -> Result<()>
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)] // mirrors the gallery query surface; a params struct adds noise for one caller
pub fn get_images( pub fn get_images(
conn: &Connection, conn: &Connection,
folder_id: Option<i64>, folder_id: Option<i64>,
@@ -1567,7 +1575,7 @@ pub fn get_images(
_ => "modified_at DESC NULLS LAST", _ => "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 favorites_flag = i64::from(favorites_only);
let embedding_failed_flag = i64::from(embedding_failed_only); let embedding_failed_flag = i64::from(embedding_failed_only);
let sql = format!( let sql = format!(
@@ -1583,9 +1591,8 @@ pub fn get_images(
AND (?4 = 0 OR favorite = 1) AND (?4 = 0 OR favorite = 1)
AND rating >= ?5 AND rating >= ?5
AND (?6 = 0 OR embedding_status = 'failed') AND (?6 = 0 OR embedding_status = 'failed')
ORDER BY {} ORDER BY {order}
LIMIT ?7 OFFSET ?8", LIMIT ?7 OFFSET ?8"
order
); );
let mut stmt = conn.prepare(&sql)?; let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map( let rows = stmt.query_map(
@@ -1613,7 +1620,7 @@ pub fn count_images(
rating_min: i64, rating_min: i64,
embedding_failed_only: bool, embedding_failed_only: bool,
) -> Result<i64> { ) -> Result<i64> {
let search_pattern = search.map(|value| format!("%{}%", value)); let search_pattern = search.map(|value| format!("%{value}%"));
let favorites_flag = i64::from(favorites_only); let favorites_flag = i64::from(favorites_only);
let embedding_failed_flag = i64::from(embedding_failed_only); let embedding_failed_flag = i64::from(embedding_failed_only);
@@ -1639,6 +1646,7 @@ pub fn count_images(
Ok(count) Ok(count)
} }
#[allow(clippy::too_many_arguments)]
pub fn search_images_by_tag( pub fn search_images_by_tag(
conn: &Connection, conn: &Connection,
query: &str, query: &str,
@@ -2388,7 +2396,7 @@ pub fn set_duplicate_scan_cache(
} }
fn ensure_column(conn: &Connection, table: &str, column: &str, definition: &str) -> Result<()> { 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([])?; let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? { while let Some(row) = rows.next()? {
let existing_name: String = row.get(1)?; let existing_name: String = row.get(1)?;
@@ -2398,7 +2406,7 @@ fn ensure_column(conn: &Connection, table: &str, column: &str, definition: &str)
} }
conn.execute( conn.execute(
&format!("ALTER TABLE {} ADD COLUMN {} {}", table, column, definition), &format!("ALTER TABLE {table} ADD COLUMN {column} {definition}"),
[], [],
)?; )?;
Ok(()) Ok(())
@@ -2425,5 +2433,5 @@ fn folder_exclusion_clause(
.map(|id| id.to_string()) .map(|id| id.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(","); .join(",");
format!("AND {}.folder_id NOT IN ({})", image_alias, id_list) format!("AND {image_alias}.folder_id NOT IN ({id_list})")
} }
+1 -1
View File
@@ -160,7 +160,7 @@ impl ClipImageEmbedder {
let ids = enc.get_ids(); let ids = enc.get_ids();
let len = ids.len().min(max_len); let len = ids.len().min(max_len);
for j in 0..len { for j in 0..len {
flat[i * max_len + j] = ids[j] as u32; flat[i * max_len + j] = ids[j];
} }
} }
+29 -35
View File
@@ -24,6 +24,7 @@ const IMAGE_EXTENSIONS: &[&str] = &[
const VIDEO_EXTENSIONS: &[&str] = &["mp4", "mov", "m4v", "webm"]; const VIDEO_EXTENSIONS: &[&str] = &["mp4", "mov", "m4v", "webm"];
const JOB_PROGRESS_EMIT_INTERVAL: Duration = Duration::from_millis(750); const JOB_PROGRESS_EMIT_INTERVAL: Duration = Duration::from_millis(750);
#[allow(dead_code)] // caption worker disabled (lib.rs)
const CAPTION_BATCH_SIZE: usize = 1; const CAPTION_BATCH_SIZE: usize = 1;
static LAST_JOB_PROGRESS_EMIT: OnceLock<Mutex<HashMap<i64, Instant>>> = OnceLock::new(); static LAST_JOB_PROGRESS_EMIT: OnceLock<Mutex<HashMap<i64, Instant>>> = OnceLock::new();
@@ -200,7 +201,7 @@ pub fn index_folder(app: AppHandle, pool: DbPool, folder_id: i64, folder_path: P
// not be silently destroyed. // not be silently destroyed.
if !folder_path.is_dir() { if !folder_path.is_dir() {
let error_msg = format!("Folder not found: {}", folder_path.display()); 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() { if let Ok(conn) = pool.get() {
let _ = db::set_folder_scan_error(&conn, folder_id, &error_msg); 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); let storage_profile = detect_storage_profile(&folder_path);
set_folder_storage_profile(folder_id, RuntimeAdaptiveProfile::new(storage_profile)); set_folder_storage_profile(folder_id, RuntimeAdaptiveProfile::new(storage_profile));
if let Err(error) = do_index(app.clone(), &pool, folder_id, folder_path) { 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() { if let Ok(conn) = pool.get() {
let _ = db::set_folder_scan_error(&conn, folder_id, &error.to_string()); let _ = db::set_folder_scan_error(&conn, folder_id, &error.to_string());
} }
@@ -254,7 +255,7 @@ pub fn start_thumbnail_worker(
Ok(true) => {} Ok(true) => {}
Ok(false) => std::thread::sleep(std::time::Duration::from_millis(250)), Ok(false) => std::thread::sleep(std::time::Duration::from_millis(250)),
Err(error) => { Err(error) => {
log::error!("Thumbnail worker error: {}", error); log::error!("Thumbnail worker error: {error}");
std::thread::sleep(std::time::Duration::from_millis(250)); 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(true) => {}
Ok(false) => std::thread::sleep(std::time::Duration::from_millis(250)), Ok(false) => std::thread::sleep(std::time::Duration::from_millis(250)),
Err(error) => { Err(error) => {
log::error!("Metadata worker error: {}", error); log::error!("Metadata worker error: {error}");
std::thread::sleep(std::time::Duration::from_millis(250)); 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(true) => {}
Ok(false) => std::thread::sleep(std::time::Duration::from_millis(500)), Ok(false) => std::thread::sleep(std::time::Duration::from_millis(500)),
Err(error) => { Err(error) => {
log::error!("Embedding worker error: {}", error); log::error!("Embedding worker error: {error}");
std::thread::sleep(std::time::Duration::from_millis(500)); 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) { pub fn start_caption_worker(app: AppHandle, pool: DbPool, app_data_dir: PathBuf) {
std::thread::spawn(move || { std::thread::spawn(move || {
let mut captioner: Option<FlorenceCaptioner> = None; let mut captioner: Option<FlorenceCaptioner> = None;
@@ -307,7 +309,7 @@ pub fn start_caption_worker(app: AppHandle, pool: DbPool, app_data_dir: PathBuf)
captioner = None; captioner = None;
} }
if let Err(error) = process_caption_batch(&app, &pool, &app_data_dir, &mut captioner) { 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; captioner = None;
} }
std::thread::sleep(std::time::Duration::from_millis(750)); 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(true) => {}
Ok(false) => std::thread::sleep(std::time::Duration::from_millis(750)), Ok(false) => std::thread::sleep(std::time::Duration::from_millis(750)),
Err(error) => { Err(error) => {
log::error!("Tagging worker error: {}", error); log::error!("Tagging worker error: {error}");
tagger_instance = None; tagger_instance = None;
std::thread::sleep(std::time::Duration::from_millis(750)); 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() { if !records.is_empty() {
let committed = commit_batch(&pool, &records)?; let committed = commit_batch(pool, &records)?;
emit_images( emit_images(
&app, &app,
&IndexedImagesBatch { &IndexedImagesBatch {
@@ -422,7 +424,7 @@ fn do_index(app: AppHandle, pool: &DbPool, folder_id: i64, folder_path: PathBuf)
images: committed, images: committed,
}, },
); );
emit_folder_job_progress(&app, &pool, &[folder_id], false); emit_folder_job_progress(&app, pool, &[folder_id], false);
} }
processed += path_chunk.len(); 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); let _ = db::clear_folder_scan_error(&conn, folder_id);
// Invalidate duplicate scan cache — any reindex can change file contents // Invalidate duplicate scan cache — any reindex can change file contents
// or the set of files, making cached duplicate groups stale. // 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, &folder_scope);
let _ = db::clear_duplicate_scan_cache(&conn, "all"); 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, done: true,
}, },
); );
emit_folder_job_progress(&app, &pool, &[folder_id], true); emit_folder_job_progress(&app, pool, &[folder_id], true);
Ok(()) Ok(())
} }
@@ -897,10 +899,7 @@ fn process_embedding_batch(
} }
} }
Err(batch_error) => { Err(batch_error) => {
log::error!( log::error!("Embedding batch fallback to per-image mode: {batch_error}");
"Embedding batch fallback to per-image mode: {}",
batch_error
);
for (job, source_path) in embeddable_jobs for (job, source_path) in embeddable_jobs
.into_iter() .into_iter()
.zip(embeddable_paths.into_iter()) .zip(embeddable_paths.into_iter())
@@ -921,7 +920,7 @@ fn process_embedding_batch(
for job in &jobs { for job in &jobs {
let embedding_result: Result<Vec<f32>> = let embedding_result: Result<Vec<f32>> =
if let Some(err) = pre_failed.remove(&job.image_id) { 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) { } else if let Some(r) = embed_results.remove(&job.image_id) {
r r
} else { } else {
@@ -963,17 +962,13 @@ fn process_embedding_batch(
let write_elapsed = write_started_at.elapsed(); let write_elapsed = write_started_at.elapsed();
let batch_elapsed = batch_started_at.elapsed(); let batch_elapsed = batch_started_at.elapsed();
log::info!( log::info!(
"Embedding batch timing: claimed {} in {:?}, infer {:?}, write {:?}, total {:?}", "Embedding batch timing: claimed {EMBEDDING_BATCH_SIZE} in {claim_elapsed:?}, infer {infer_elapsed:?}, write {write_elapsed:?}, total {batch_elapsed:?}"
EMBEDDING_BATCH_SIZE,
claim_elapsed,
infer_elapsed,
write_elapsed,
batch_elapsed
); );
Ok(true) Ok(true)
} }
#[allow(dead_code)] // caption worker disabled (lib.rs)
fn process_caption_batch( fn process_caption_batch(
app: &AppHandle, app: &AppHandle,
pool: &DbPool, pool: &DbPool,
@@ -1186,7 +1181,7 @@ fn process_tagging_batch(
tx.commit()?; tx.commit()?;
Ok(updated_images) 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 // The DB write failed. Try to requeue the claimed jobs so they aren't
// left stuck in 'processing' until the next app restart. // left stuck in 'processing' until the next app restart.
let image_ids: Vec<i64> = jobs.iter().map(|job| job.image_id).collect(); let image_ids: Vec<i64> = jobs.iter().map(|job| job.image_id).collect();
@@ -1194,7 +1189,6 @@ fn process_tagging_batch(
let conn = pool.get()?; let conn = pool.get()?;
db::requeue_tagging_jobs(&conn, &image_ids) db::requeue_tagging_jobs(&conn, &image_ids)
}); });
Err(db_err)
})?; })?;
if !updated_images.is_empty() { 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) { 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::<Vec<_>>(); let mut unique_folder_ids = folder_ids.to_vec();
unique_folder_ids.sort_unstable(); unique_folder_ids.sort_unstable();
unique_folder_ids.dedup(); unique_folder_ids.dedup();
@@ -1401,7 +1395,7 @@ impl WatcherHandle {
let mut w = self.inner.watcher.lock().unwrap(); let mut w = self.inner.watcher.lock().unwrap();
if path.is_dir() { if path.is_dir() {
if let Err(e) = w.watch(&path, RecursiveMode::Recursive) { 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); let _ = w.unwatch(old_path);
if new_path.is_dir() { if new_path.is_dir() {
if let Err(e) = w.watch(&new_path, RecursiveMode::Recursive) { 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() { for path in map.keys() {
if path.is_dir() { if path.is_dir() {
if let Err(e) = w.watch(path, RecursiveMode::Recursive) { 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() { let conn = match pool.get() {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
log::error!("Watcher: DB pool error: {}", e); log::error!("Watcher: DB pool error: {e}");
return; return;
} }
}; };
@@ -1632,7 +1626,7 @@ fn process_watcher_path(
} }
} }
Ok(_) => {} Ok(_) => {}
Err(e) => log::error!("Watcher: commit error for {:?}: {}", path, e), Err(e) => log::error!("Watcher: commit error for {path:?}: {e}"),
} }
} else { } else {
// File removed from disk — clean up DB row and thumbnail. // 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 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() { let conn = match pool.get() {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
log::error!("Watcher rename: DB pool error: {}", e); log::error!("Watcher rename: DB pool error: {e}");
return; return;
} }
}; };
@@ -1696,7 +1690,7 @@ fn process_watcher_rename(
return; return;
} }
Err(e) => { Err(e) => {
log::error!("Watcher rename: DB lookup error: {}", e); log::error!("Watcher rename: DB lookup error: {e}");
return; return;
} }
}; };
@@ -1721,7 +1715,7 @@ fn process_watcher_rename(
new_filename, new_filename,
effective_thumb, effective_thumb,
) { ) {
log::error!("Watcher rename: DB update error: {}", e); log::error!("Watcher rename: DB update error: {e}");
return; return;
} }
@@ -1734,6 +1728,6 @@ fn process_watcher_rename(
images: vec![record], 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}"),
} }
} }
+2 -3
View File
@@ -65,14 +65,13 @@ pub fn run() {
let backfilled = let backfilled =
db::backfill_embedding_jobs(&conn).expect("Failed to backfill embedding jobs"); db::backfill_embedding_jobs(&conn).expect("Failed to backfill embedding jobs");
if backfilled > 0 { 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) let (orphaned_vectors, missing_vectors) = db::repair_embedding_consistency(&conn)
.expect("Failed to repair embedding consistency"); .expect("Failed to repair embedding consistency");
if orphaned_vectors > 0 || missing_vectors > 0 { if orphaned_vectors > 0 || missing_vectors > 0 {
log::info!( log::info!(
"Repaired embedding consistency: removed {} orphaned vectors, requeued {} missing vectors.", "Repaired embedding consistency: removed {orphaned_vectors} orphaned vectors, requeued {missing_vectors} missing vectors."
orphaned_vectors, missing_vectors
); );
} }
} }
+1 -5
View File
@@ -49,11 +49,7 @@ impl MediaTools {
total_bytes, total_bytes,
downloaded_bytes, downloaded_bytes,
} => { } => {
log::info!( log::info!("Downloading bundled FFmpeg: {downloaded_bytes}/{total_bytes} bytes");
"Downloading bundled FFmpeg: {}/{} bytes",
downloaded_bytes,
total_bytes
);
} }
FfmpegDownloadProgressEvent::UnpackingArchive => { FfmpegDownloadProgressEvent::UnpackingArchive => {
log::info!("Unpacking bundled FFmpeg..."); log::info!("Unpacking bundled FFmpeg...");
+2 -2
View File
@@ -1,4 +1,4 @@
use std::path::{Path, PathBuf}; use std::path::Path;
use sysinfo::{DiskKind, Disks}; use sysinfo::{DiskKind, Disks};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[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(); let path_str = path.to_string_lossy().to_lowercase();
if path_str.starts_with("\\\\") { if path_str.starts_with("\\\\") {
return StorageProfile::Conservative; return StorageProfile::Conservative;
+1 -1
View File
@@ -481,7 +481,7 @@ impl WdTagger {
.try_extract_tensor::<f32>() .try_extract_tensor::<f32>()
.map_err(|error| anyhow::anyhow!("{error}"))?; .map_err(|error| anyhow::anyhow!("{error}"))?;
let probs: &[f32] = &probabilities; let probs: &[f32] = probabilities;
if probs.len() != self.labels.len() { if probs.len() != self.labels.len() {
anyhow::bail!( anyhow::bail!(
+16 -18
View File
@@ -237,9 +237,7 @@ pub fn generate_video_thumbnail(
} }
Err(anyhow!( Err(anyhow!(
"ffmpeg failed generating poster for {}: {}", "ffmpeg failed generating poster for {path_str}: {last_error}"
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 { fn thumb_path_with_ext(cache_dir: &Path, input_path: &str, extension: &str) -> PathBuf {
let hash = hash_path(input_path); let hash = hash_path(input_path);
cache_dir.join(format!("{}.{}", hash, extension)) cache_dir.join(format!("{hash}.{extension}"))
} }
fn hash_path(s: &str) -> String { fn hash_path(s: &str) -> String {
format!("{:016x}", xxhash_rust::xxh3::xxh3_64(s.as_bytes())) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -302,17 +314,3 @@ mod tests {
assert_eq!((thumb_w, thumb_h), (320, 240)); 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)
}
}
+12 -6
View File
@@ -10,7 +10,14 @@ static SQLITE_VEC_INIT: Once = Once::new();
pub fn register_sqlite_vec() { pub fn register_sqlite_vec() {
SQLITE_VEC_INIT.call_once(|| unsafe { 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!( conn.execute_batch(&format!(
"CREATE VIRTUAL TABLE IF NOT EXISTS image_vec USING vec0( "CREATE VIRTUAL TABLE IF NOT EXISTS image_vec USING vec0(
image_id INTEGER PRIMARY KEY, 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( CREATE VIRTUAL TABLE IF NOT EXISTS caption_vec USING vec0(
image_id INTEGER PRIMARY KEY, image_id INTEGER PRIMARY KEY,
embedding FLOAT[{}] distance_metric=cosine embedding FLOAT[{CLIP_VECTOR_DIM}] distance_metric=cosine
);", );"
CLIP_VECTOR_DIM, CLIP_VECTOR_DIM
))?; ))?;
Ok(()) Ok(())
} }
@@ -465,7 +471,7 @@ pub fn has_image_vector(conn: &Connection, image_id: i64) -> Result<bool> {
#[allow(dead_code)] #[allow(dead_code)]
fn pack_f32(values: &[f32]) -> Vec<u8> { fn pack_f32(values: &[f32]) -> Vec<u8> {
let mut out = Vec::with_capacity(values.len() * std::mem::size_of::<f32>()); let mut out = Vec::with_capacity(std::mem::size_of_val(values));
for value in values { for value in values {
out.extend_from_slice(&value.to_le_bytes()); out.extend_from_slice(&value.to_le_bytes());
} }