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:
@@ -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
|
||||||
|
|||||||
+126
-127
@@ -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,132 +1038,131 @@ 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;
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use xxhash_rust::xxh3::{xxh3_64, Xxh3};
|
||||||
use xxhash_rust::xxh3::{xxh3_64, Xxh3};
|
|
||||||
|
|
||||||
const WINDOW: usize = 16 * 1024; // 16 KB per sample window
|
const WINDOW: usize = 16 * 1024; // 16 KB per sample window
|
||||||
const N: usize = 4; // windows at 0%, 33%, 66%, 100%
|
const N: usize = 4; // windows at 0%, 33%, 66%, 100%
|
||||||
const COVERED: usize = WINDOW * N; // 64 KB total; also full-coverage threshold
|
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
|
// Hash four evenly-spaced 16 KB windows. For files ≤ 64 KB this is
|
||||||
// equivalent to hashing the entire file.
|
// equivalent to hashing the entire file.
|
||||||
fn sample(mmap: &[u8]) -> u64 {
|
fn sample(mmap: &[u8]) -> u64 {
|
||||||
if mmap.len() <= COVERED {
|
if mmap.len() <= COVERED {
|
||||||
return xxh3_64(mmap);
|
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 ─────────────────────────────────────────────────
|
// ── Phase 1: stat ─────────────────────────────────────────────────
|
||||||
let stat_counter = AtomicUsize::new(0);
|
let stat_counter = AtomicUsize::new(0);
|
||||||
let stat_skipped = AtomicUsize::new(0);
|
let stat_skipped = AtomicUsize::new(0);
|
||||||
let sized: Vec<(i64, String, u64)> = records
|
let sized: Vec<(i64, String, u64)> = records
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.filter_map(|r| {
|
.filter_map(|r| {
|
||||||
let result = match std::fs::metadata(&r.path) {
|
let result = match std::fs::metadata(&r.path) {
|
||||||
Ok(metadata) => Some((r.id, r.path.clone(), metadata.len())),
|
Ok(metadata) => Some((r.id, r.path.clone(), metadata.len())),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
stat_skipped.fetch_add(1, Ordering::Relaxed);
|
stat_skipped.fetch_add(1, Ordering::Relaxed);
|
||||||
None
|
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),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
result
|
||||||
let done = stat_counter.fetch_add(1, Ordering::Relaxed) + 1;
|
})
|
||||||
if done % 100 == 0 || done == total {
|
.collect();
|
||||||
let _ = app_hash.emit(
|
let stat_skipped = stat_skipped.load(Ordering::Relaxed);
|
||||||
"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);
|
|
||||||
|
|
||||||
let mut by_size: HashMap<u64, Vec<usize>> = HashMap::new();
|
let mut by_size: HashMap<u64, Vec<usize>> = HashMap::new();
|
||||||
for (i, (_, _, size)) in sized.iter().enumerate() {
|
for (i, (_, _, size)) in sized.iter().enumerate() {
|
||||||
by_size.entry(*size).or_default().push(i);
|
by_size.entry(*size).or_default().push(i);
|
||||||
}
|
}
|
||||||
let candidates: Vec<usize> = by_size
|
let candidates: Vec<usize> = by_size
|
||||||
.into_values()
|
.into_values()
|
||||||
.filter(|g| g.len() > 1)
|
.filter(|g| g.len() > 1)
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if candidates.is_empty() {
|
if candidates.is_empty() {
|
||||||
return (vec![], stat_skipped, 0);
|
return (vec![], stat_skipped, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Phase 2: sample hash ──────────────────────────────────────────
|
// ── Phase 2: sample hash ──────────────────────────────────────────
|
||||||
let c_total = candidates.len();
|
let c_total = candidates.len();
|
||||||
let _ = app_hash.emit(
|
let _ = app_hash.emit(
|
||||||
"duplicate_scan_progress",
|
"duplicate_scan_progress",
|
||||||
DuplicateScanProgress {
|
DuplicateScanProgress {
|
||||||
phase: "hashing".to_string(),
|
phase: "hashing".to_string(),
|
||||||
processed: 0,
|
processed: 0,
|
||||||
total: c_total,
|
total: c_total,
|
||||||
skipped: stat_skipped,
|
skipped: stat_skipped,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let counter = AtomicUsize::new(0);
|
let counter = AtomicUsize::new(0);
|
||||||
let hash_skipped = AtomicUsize::new(0);
|
let hash_skipped = AtomicUsize::new(0);
|
||||||
|
|
||||||
let pairs = candidates
|
let pairs = candidates
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.filter_map(|&idx| {
|
.filter_map(|&idx| {
|
||||||
let (id, path, size) = &sized[idx];
|
let (id, path, size) = &sized[idx];
|
||||||
let result = if *size == 0 {
|
let result = if *size == 0 {
|
||||||
Some((xxh3_64(&[]), *size, *id, path.clone()))
|
Some((xxh3_64(&[]), *size, *id, path.clone()))
|
||||||
} else {
|
} else {
|
||||||
std::fs::File::open(path)
|
std::fs::File::open(path)
|
||||||
.ok()
|
.ok()
|
||||||
// SAFETY: read-only; no external truncation expected during scan.
|
// SAFETY: read-only; no external truncation expected during scan.
|
||||||
.and_then(|file| unsafe { Mmap::map(&file).ok() })
|
.and_then(|file| unsafe { Mmap::map(&file).ok() })
|
||||||
.map(|mmap| (sample(&mmap), *size, *id, path.clone()))
|
.map(|mmap| (sample(&mmap), *size, *id, path.clone()))
|
||||||
};
|
};
|
||||||
if result.is_none() {
|
if result.is_none() {
|
||||||
hash_skipped.fetch_add(1, Ordering::Relaxed);
|
hash_skipped.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
let done = counter.fetch_add(1, Ordering::Relaxed) + 1;
|
let done = counter.fetch_add(1, Ordering::Relaxed) + 1;
|
||||||
if done % 100 == 0 || done == c_total {
|
if done % 100 == 0 || done == c_total {
|
||||||
let _ = app_hash.emit(
|
let _ = app_hash.emit(
|
||||||
"duplicate_scan_progress",
|
"duplicate_scan_progress",
|
||||||
DuplicateScanProgress {
|
DuplicateScanProgress {
|
||||||
phase: "hashing".to_string(),
|
phase: "hashing".to_string(),
|
||||||
processed: done,
|
processed: done,
|
||||||
total: c_total,
|
total: c_total,
|
||||||
skipped: stat_skipped + hash_skipped.load(Ordering::Relaxed),
|
skipped: stat_skipped + hash_skipped.load(Ordering::Relaxed),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
(
|
(
|
||||||
pairs,
|
pairs,
|
||||||
stat_skipped + hash_skipped.load(Ordering::Relaxed),
|
stat_skipped + hash_skipped.load(Ordering::Relaxed),
|
||||||
c_total,
|
c_total,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// Group by (sample_hash, file_size).
|
// Group by (sample_hash, file_size).
|
||||||
let mut size_hash_map: std::collections::HashMap<(u64, u64), Vec<(i64, String)>> =
|
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)| {
|
.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
@@ -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})")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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...");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user