From 8d61dc1139901103dc8a2441296738ae38527bf9 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Tue, 11 Nov 2025 07:07:06 -0800 Subject: [PATCH] feat: implement thumbnail generation for linked content identities and improve logging in responder --- core/src/ops/indexing/responder.rs | 58 +++++++- core/src/ops/media/thumbnail/mod.rs | 129 ++++++++++++++++++ .../ts-client/src/hooks/useNormalizedCache.ts | 10 -- 3 files changed, 183 insertions(+), 14 deletions(-) diff --git a/core/src/ops/indexing/responder.rs b/core/src/ops/indexing/responder.rs index bed2b4738..7a34327c2 100644 --- a/core/src/ops/indexing/responder.rs +++ b/core/src/ops/indexing/responder.rs @@ -342,7 +342,7 @@ async fn handle_create( debug!("✓ Generated content hash: {}", content_hash); // Link the content identity - if let Err(e) = EntryProcessor::link_to_content_identity( + match EntryProcessor::link_to_content_identity( ctx, entry_id, path, @@ -351,9 +351,59 @@ async fn handle_create( ) .await { - warn!("Failed to link content identity for {}: {}", path.display(), e); - } else { - debug!("✓ Linked content identity for entry {}", entry_id); + Ok(link_result) => { + debug!("✓ Linked content identity for entry {}", entry_id); + + // Generate thumbnails for the file + if let Some(library) = context.get_library(library_id).await { + debug!("→ Generating thumbnails for: {}", path.display()); + + // Get MIME type from content identity + let mime_type = if let Ok(Some(ci)) = crate::infra::db::entities::content_identity::Entity::find_by_id(link_result.content_identity.id) + .one(ctx.library_db()) + .await + { + if let Some(mime_id) = ci.mime_type_id { + if let Ok(Some(mime_record)) = crate::infra::db::entities::mime_type::Entity::find_by_id(mime_id) + .one(ctx.library_db()) + .await + { + Some(mime_record.mime_type) + } else { + None + } + } else { + None + } + } else { + None + }; + + if let Some(mime) = mime_type { + match crate::ops::media::thumbnail::generate_thumbnails_for_file( + &library, + &link_result.content_identity.uuid.expect("ContentIdentity should have UUID"), + path, + &mime, + ) + .await + { + Ok(count) if count > 0 => { + debug!("✓ Generated {} thumbnails for: {}", count, path.display()); + } + Ok(_) => { + debug!("No thumbnails generated for: {}", path.display()); + } + Err(e) => { + debug!("Thumbnail generation failed for {}: {}", path.display(), e); + } + } + } + } + } + Err(e) => { + warn!("Failed to link content identity for {}: {}", path.display(), e); + } } } else { debug!("✗ Failed to generate content hash for {}", path.display()); diff --git a/core/src/ops/media/thumbnail/mod.rs b/core/src/ops/media/thumbnail/mod.rs index 9ea37dc4b..eb75a6b28 100644 --- a/core/src/ops/media/thumbnail/mod.rs +++ b/core/src/ops/media/thumbnail/mod.rs @@ -20,3 +20,132 @@ pub use generator::{ImageGenerator, ThumbnailGenerator, ThumbnailInfo, VideoGene pub use job::{ThumbnailJob, ThumbnailJobConfig}; pub use state::{ThumbnailEntry, ThumbnailPhase, ThumbnailState, ThumbnailStats}; pub use utils::ThumbnailUtils; + +use crate::library::Library; +use crate::ops::sidecar::types::SidecarKind; +use std::path::Path; +use std::sync::Arc; +use uuid::Uuid; + +/// Generate thumbnails for a single file (optimized for watcher/responder use) +/// +/// This function generates the default thumbnail variants for a single file +/// and registers them as sidecars. It's designed to be called inline for +/// individual file creates/updates rather than dispatching a job. +/// +/// # Arguments +/// * `library` - The library context +/// * `content_uuid` - The UUID of the content identity +/// * `source_path` - The filesystem path to the source file +/// * `mime_type` - The MIME type of the file +/// +/// # Returns +/// Number of thumbnails successfully generated +pub async fn generate_thumbnails_for_file( + library: &Arc, + content_uuid: &Uuid, + source_path: &Path, + mime_type: &str, +) -> ThumbnailResult { + use tracing::{debug, warn}; + + // Check if thumbnail generation is supported for this file type + if !ThumbnailUtils::is_thumbnail_supported(mime_type) { + debug!("Thumbnail generation not supported for MIME type: {}", mime_type); + return Ok(0); + } + + // Get sidecar manager + let sidecar_manager = library + .core_context() + .get_sidecar_manager() + .await + .ok_or_else(|| ThumbnailError::other("SidecarManager not available"))?; + + // Create thumbnail generator for this MIME type + let generator = ThumbnailGenerator::for_mime_type(mime_type)?; + + // Generate default variants (grid@1x, grid@2x, detail@1x) + let variants = ThumbnailVariants::defaults(); + let mut generated_count = 0; + + for variant_config in variants { + // Check if thumbnail already exists + if sidecar_manager + .exists( + &library.id(), + content_uuid, + &SidecarKind::Thumb, + &variant_config.variant, + &variant_config.format(), + ) + .await + .unwrap_or(false) + { + debug!( + "Thumbnail already exists for {}: {}", + content_uuid, + variant_config.variant.as_str() + ); + continue; + } + + // Compute output path + let output_path = sidecar_manager + .compute_path( + &library.id(), + content_uuid, + &SidecarKind::Thumb, + &variant_config.variant, + &variant_config.format(), + ) + .await + .map_err(|e| ThumbnailError::other(format!("Failed to compute path: {}", e)))?; + + // Generate thumbnail + match generator + .generate(source_path, &output_path.absolute_path, variant_config.size, variant_config.quality) + .await + { + Ok(thumbnail_info) => { + // Record the sidecar in the database + if let Err(e) = sidecar_manager + .record_sidecar( + library, + content_uuid, + &SidecarKind::Thumb, + &variant_config.variant, + &variant_config.format(), + thumbnail_info.size_bytes as u64, + None, + ) + .await + { + warn!( + "Failed to record sidecar for {}: {}", + variant_config.variant.as_str(), + e + ); + } else { + debug!( + "✓ Generated thumbnail {}: {}x{}", + variant_config.variant.as_str(), + thumbnail_info.dimensions.0, + thumbnail_info.dimensions.1 + ); + generated_count += 1; + } + } + Err(e) => { + warn!( + "Failed to generate thumbnail {} for {}: {}", + variant_config.variant.as_str(), + source_path.display(), + e + ); + } + } + } + + Ok(generated_count) +} diff --git a/packages/ts-client/src/hooks/useNormalizedCache.ts b/packages/ts-client/src/hooks/useNormalizedCache.ts index 11ec8accb..b90fb0cd1 100644 --- a/packages/ts-client/src/hooks/useNormalizedCache.ts +++ b/packages/ts-client/src/hooks/useNormalizedCache.ts @@ -224,18 +224,8 @@ export function useNormalizedCache({ // The filter checks parent path for Physical paths, which is correct for new files const shouldAppend = resourceFilter(resource); - console.log('[useNormalizedCache] Batch - checking resource:', { - name: resource.name, - id: resource.id, - shouldAppend, - seenIds: Array.from(seenIds) - }); - if (shouldAppend) { newData.push(resource); - console.log('[useNormalizedCache] ✓ Appended new resource:', resource.name); - } else { - console.log('[useNormalizedCache] ✗ Rejected resource (filter returned false):', resource.name); } } }