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:
+126
-127
@@ -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<HashedCandidate>, 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<u64, Vec<usize>> = HashMap::new();
|
||||
for (i, (_, _, size)) in sized.iter().enumerate() {
|
||||
by_size.entry(*size).or_default().push(i);
|
||||
}
|
||||
let candidates: Vec<usize> = by_size
|
||||
.into_values()
|
||||
.filter(|g| g.len() > 1)
|
||||
.flatten()
|
||||
.collect();
|
||||
let mut by_size: HashMap<u64, Vec<usize>> = HashMap::new();
|
||||
for (i, (_, _, size)) in sized.iter().enumerate() {
|
||||
by_size.entry(*size).or_default().push(i);
|
||||
}
|
||||
let candidates: Vec<usize> = 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<i64>,
|
||||
) -> Result<Option<DuplicateScanCache>, 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<i64>,
|
||||
) -> 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<f32>) {
|
||||
fn normalize(v: &mut [f32]) {
|
||||
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if norm > 1e-10 {
|
||||
v.iter_mut().for_each(|x| *x /= norm);
|
||||
|
||||
+16
-8
@@ -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<i64>) -> Result<u
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // caption worker disabled (lib.rs)
|
||||
pub fn is_caption_job_cancelled(conn: &Connection, image_id: i64) -> Result<bool> {
|
||||
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<i64>,
|
||||
@@ -891,6 +896,7 @@ fn get_pending_caption_jobs_excluding(
|
||||
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // caption worker disabled (lib.rs)
|
||||
pub fn claim_caption_jobs(
|
||||
conn: &mut Connection,
|
||||
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
|
||||
/// 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>> {
|
||||
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<i64>,
|
||||
@@ -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<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 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::<Vec<_>>()
|
||||
.join(",");
|
||||
format!("AND {}.folder_id NOT IN ({})", image_alias, id_list)
|
||||
format!("AND {image_alias}.folder_id NOT IN ({id_list})")
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-35
@@ -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<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.
|
||||
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<FlorenceCaptioner> = 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<Vec<f32>> =
|
||||
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<i64> = 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::<Vec<_>>();
|
||||
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}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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...");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -481,7 +481,7 @@ impl WdTagger {
|
||||
.try_extract_tensor::<f32>()
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
|
||||
let probs: &[f32] = &probabilities;
|
||||
let probs: &[f32] = probabilities;
|
||||
|
||||
if probs.len() != self.labels.len() {
|
||||
anyhow::bail!(
|
||||
|
||||
+16
-18
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-6
@@ -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<bool> {
|
||||
|
||||
#[allow(dead_code)]
|
||||
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 {
|
||||
out.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user