pinakes-core: improve media management features; various configuration improvements
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I2d1f04f13970d21c36067f30bc04a9176a6a6964
This commit is contained in:
parent
cfdc3d0622
commit
e02c15490e
31 changed files with 1087 additions and 197 deletions
|
|
@ -113,6 +113,24 @@ fn row_to_media_item(row: &Row) -> rusqlite::Result<MediaItem> {
|
|||
custom_fields: HashMap::new(), // loaded separately
|
||||
// file_mtime may not be present in all queries, so handle gracefully
|
||||
file_mtime: row.get::<_, Option<i64>>("file_mtime").unwrap_or(None),
|
||||
|
||||
// Photo-specific fields (may not be present in all queries)
|
||||
date_taken: row
|
||||
.get::<_, Option<String>>("date_taken")
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|s| DateTime::parse_from_rfc3339(&s).ok())
|
||||
.map(|dt| dt.with_timezone(&Utc)),
|
||||
latitude: row.get::<_, Option<f64>>("latitude").ok().flatten(),
|
||||
longitude: row.get::<_, Option<f64>>("longitude").ok().flatten(),
|
||||
camera_make: row.get::<_, Option<String>>("camera_make").ok().flatten(),
|
||||
camera_model: row.get::<_, Option<String>>("camera_model").ok().flatten(),
|
||||
rating: row.get::<_, Option<i32>>("rating").ok().flatten(),
|
||||
perceptual_hash: row
|
||||
.get::<_, Option<String>>("perceptual_hash")
|
||||
.ok()
|
||||
.flatten(),
|
||||
|
||||
created_at: parse_datetime(&created_str),
|
||||
updated_at: parse_datetime(&updated_str),
|
||||
})
|
||||
|
|
@ -610,8 +628,9 @@ impl StorageBackend for SqliteBackend {
|
|||
db.execute(
|
||||
"INSERT INTO media_items (id, path, file_name, media_type, content_hash, \
|
||||
file_size, title, artist, album, genre, year, duration_secs, description, \
|
||||
thumbnail_path, file_mtime, created_at, updated_at) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)",
|
||||
thumbnail_path, file_mtime, date_taken, latitude, longitude, camera_make, \
|
||||
camera_model, rating, perceptual_hash, created_at, updated_at) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24)",
|
||||
params![
|
||||
item.id.0.to_string(),
|
||||
item.path.to_string_lossy().as_ref(),
|
||||
|
|
@ -630,6 +649,13 @@ impl StorageBackend for SqliteBackend {
|
|||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string()),
|
||||
item.file_mtime,
|
||||
item.date_taken.as_ref().map(|d| d.to_rfc3339()),
|
||||
item.latitude,
|
||||
item.longitude,
|
||||
item.camera_make,
|
||||
item.camera_model,
|
||||
item.rating,
|
||||
item.perceptual_hash,
|
||||
item.created_at.to_rfc3339(),
|
||||
item.updated_at.to_rfc3339(),
|
||||
],
|
||||
|
|
@ -781,7 +807,9 @@ impl StorageBackend for SqliteBackend {
|
|||
"UPDATE media_items SET path = ?2, file_name = ?3, media_type = ?4, \
|
||||
content_hash = ?5, file_size = ?6, title = ?7, artist = ?8, album = ?9, \
|
||||
genre = ?10, year = ?11, duration_secs = ?12, description = ?13, \
|
||||
thumbnail_path = ?14, file_mtime = ?15, updated_at = ?16 WHERE id = ?1",
|
||||
thumbnail_path = ?14, file_mtime = ?15, date_taken = ?16, latitude = ?17, \
|
||||
longitude = ?18, camera_make = ?19, camera_model = ?20, rating = ?21, \
|
||||
perceptual_hash = ?22, updated_at = ?23 WHERE id = ?1",
|
||||
params![
|
||||
item.id.0.to_string(),
|
||||
item.path.to_string_lossy().as_ref(),
|
||||
|
|
@ -800,6 +828,13 @@ impl StorageBackend for SqliteBackend {
|
|||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string()),
|
||||
item.file_mtime,
|
||||
item.date_taken.as_ref().map(|d| d.to_rfc3339()),
|
||||
item.latitude,
|
||||
item.longitude,
|
||||
item.camera_make,
|
||||
item.camera_model,
|
||||
item.rating,
|
||||
item.perceptual_hash,
|
||||
item.updated_at.to_rfc3339(),
|
||||
],
|
||||
)?;
|
||||
|
|
@ -1534,6 +1569,77 @@ impl StorageBackend for SqliteBackend {
|
|||
.map_err(|e| PinakesError::Database(e.to_string()))?
|
||||
}
|
||||
|
||||
async fn find_perceptual_duplicates(&self, threshold: u32) -> Result<Vec<Vec<MediaItem>>> {
|
||||
let conn = Arc::clone(&self.conn);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let db = conn
|
||||
.lock()
|
||||
.map_err(|e| PinakesError::Database(e.to_string()))?;
|
||||
|
||||
// Get all images with perceptual hashes
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT * FROM media_items WHERE perceptual_hash IS NOT NULL ORDER BY id",
|
||||
)?;
|
||||
let mut items: Vec<MediaItem> = stmt
|
||||
.query_map([], row_to_media_item)?
|
||||
.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
|
||||
load_custom_fields_batch(&db, &mut items)?;
|
||||
|
||||
// Compare each pair and build groups
|
||||
use image_hasher::ImageHash;
|
||||
let mut groups: Vec<Vec<MediaItem>> = Vec::new();
|
||||
let mut grouped_indices: std::collections::HashSet<usize> =
|
||||
std::collections::HashSet::new();
|
||||
|
||||
for i in 0..items.len() {
|
||||
if grouped_indices.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let hash_a = match &items[i].perceptual_hash {
|
||||
Some(h) => match ImageHash::<Vec<u8>>::from_base64(h) {
|
||||
Ok(hash) => hash,
|
||||
Err(_) => continue,
|
||||
},
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let mut group = vec![items[i].clone()];
|
||||
grouped_indices.insert(i);
|
||||
|
||||
for (j, item_j) in items.iter().enumerate().skip(i + 1) {
|
||||
if grouped_indices.contains(&j) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let hash_b = match &item_j.perceptual_hash {
|
||||
Some(h) => match ImageHash::<Vec<u8>>::from_base64(h) {
|
||||
Ok(hash) => hash,
|
||||
Err(_) => continue,
|
||||
},
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let distance = hash_a.dist(&hash_b);
|
||||
if distance <= threshold {
|
||||
group.push(item_j.clone());
|
||||
grouped_indices.insert(j);
|
||||
}
|
||||
}
|
||||
|
||||
// Only add groups with more than one item (actual duplicates)
|
||||
if group.len() > 1 {
|
||||
groups.push(group);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(groups)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| PinakesError::Database(e.to_string()))?
|
||||
}
|
||||
|
||||
// -- Database management -----------------------------------------------
|
||||
|
||||
async fn database_stats(&self) -> Result<crate::storage::DatabaseStats> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue