From 073b5ed418960733f2f0846448e499c836405d64 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 26 Jan 2026 11:42:33 -0500 Subject: [PATCH 1/5] Added gps-summary endpoint for Map integration --- src/data/mod.rs | 14 +++++++++ src/database/mod.rs | 71 ++++++++++++++++++++++++++++++++++++++++++ src/files.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 4 +++ 4 files changed, 164 insertions(+) diff --git a/src/data/mod.rs b/src/data/mod.rs index 6c7460c..53e7020 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -347,6 +347,20 @@ pub struct GetTagsRequest { pub path: Option, } +#[derive(Debug, Serialize)] +pub struct GpsPhotoSummary { + pub path: String, + pub lat: f64, + pub lon: f64, + pub date_taken: Option, +} + +#[derive(Debug, Serialize)] +pub struct GpsPhotosResponse { + pub photos: Vec, + pub total: usize, +} + #[cfg(test)] mod tests { use super::Claims; diff --git a/src/database/mod.rs b/src/database/mod.rs index b93e396..7d058d8 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -307,6 +307,15 @@ pub trait ExifDao: Sync + Send { limit: Option, offset: i64, ) -> Result<(Vec, i64), DbError>; + + /// Get all photos with GPS coordinates + /// Returns Vec<(file_path, latitude, longitude, date_taken)> + fn get_all_with_gps( + &mut self, + context: &opentelemetry::Context, + base_path: &str, + recursive: bool, + ) -> Result)>, DbError>; } pub struct SqliteExifDao { @@ -653,4 +662,66 @@ impl ExifDao for SqliteExifDao { }) .map_err(|_| DbError::new(DbErrorKind::QueryError)) } + + fn get_all_with_gps( + &mut self, + context: &opentelemetry::Context, + base_path: &str, + recursive: bool, + ) -> Result)>, DbError> { + trace_db_call(context, "query", "get_all_with_gps", |span| { + use opentelemetry::KeyValue; + use opentelemetry::trace::Span; + use schema::image_exif::dsl::*; + + span.set_attributes(vec![ + KeyValue::new("base_path", base_path.to_string()), + KeyValue::new("recursive", recursive.to_string()), + ]); + + let connection = &mut *self.connection.lock().unwrap(); + + // Query all photos with non-null GPS coordinates + let mut query = image_exif + .filter(gps_latitude.is_not_null().and(gps_longitude.is_not_null())) + .into_boxed(); + + // Apply path filtering + // If base_path is empty or "/", return all GPS photos (no filter) + // Otherwise filter by path prefix + if !base_path.is_empty() && base_path != "/" { + // Match base path as prefix (with wildcard) + query = query.filter(file_path.like(format!("{}%", base_path))); + + span.set_attribute(KeyValue::new("path_filter_applied", true)); + } else { + span.set_attribute(KeyValue::new("path_filter_applied", false)); + span.set_attribute(KeyValue::new("returning_all_gps_photos", true)); + } + + // Load full ImageExif records + let results: Vec = query + .load::(connection) + .map_err(|e| anyhow::anyhow!("GPS query error: {}", e))?; + + // Convert to tuple format (path, lat, lon, date_taken) + // Filter out any rows where GPS is still None (shouldn't happen due to filter) + // Cast f32 GPS values to f64 for API compatibility + let filtered: Vec<(String, f64, f64, Option)> = results + .into_iter() + .filter_map(|exif| { + if let (Some(lat_val), Some(lon_val)) = (exif.gps_latitude, exif.gps_longitude) { + Some((exif.file_path, lat_val as f64, lon_val as f64, exif.date_taken)) + } else { + None + } + }) + .collect(); + + span.set_attribute(KeyValue::new("result_count", filtered.len() as i64)); + + Ok(filtered) + }) + .map_err(|_| DbError::new(DbErrorKind::QueryError)) + } } diff --git a/src/files.rs b/src/files.rs index bfcc754..d925742 100644 --- a/src/files.rs +++ b/src/files.rs @@ -951,6 +951,77 @@ fn is_path_above_base_dir + Debug>( ) } +/// Handler for GPS summary endpoint +/// Returns lightweight GPS data for all photos with coordinates +pub async fn get_gps_summary( + _: Claims, + request: HttpRequest, + req: web::Query, + state: web::Data, + exif_dao: web::Data>>, +) -> Result { + use crate::data::{GpsPhotoSummary, GpsPhotosResponse}; + + let parent_cx = extract_context_from_request(&request); + let tracer = global_tracer(); + let mut span = tracer + .span_builder("get_gps_summary") + .start_with_context(&tracer, &parent_cx); + + span.set_attribute(KeyValue::new("path", req.path.clone())); + span.set_attribute(KeyValue::new("recursive", req.recursive.unwrap_or(false).to_string())); + + let cx = opentelemetry::Context::current_with_span(span); + + // The database stores relative paths, so we use the path as-is + // Normalize empty path or "/" to return all GPS photos + let requested_path = if req.path.is_empty() || req.path == "/" { + "" + } else { + // Just do basic validation to prevent path traversal + if req.path.contains("..") { + warn!("Path traversal attempt: {}", req.path); + cx.span().set_status(Status::error("Invalid path")); + return Ok(HttpResponse::Forbidden().json(serde_json::json!({ + "error": "Invalid path" + }))); + } + req.path.as_str() + }; + + let recursive = req.recursive.unwrap_or(false); + info!("Fetching GPS photos for path='{}' recursive={}", requested_path, recursive); + + // Query database for all photos with GPS + let mut exif_dao_guard = exif_dao.lock().expect("Unable to get ExifDao"); + match exif_dao_guard.get_all_with_gps(&cx, requested_path, recursive) { + Ok(gps_data) => { + let photos: Vec = gps_data + .into_iter() + .map(|(path, lat, lon, date_taken)| GpsPhotoSummary { + path, + lat, + lon, + date_taken, + }) + .collect(); + + let total = photos.len(); + cx.span().set_attribute(KeyValue::new("result_count", total as i64)); + cx.span().set_status(Status::Ok); + + Ok(HttpResponse::Ok().json(GpsPhotosResponse { photos, total })) + } + Err(e) => { + error!("Error querying GPS data: {:?}", e); + cx.span().set_status(Status::error(format!("Database error: {:?}", e))); + Ok(HttpResponse::InternalServerError().json(serde_json::json!({ + "error": "Failed to query GPS data" + }))) + } + } +} + pub async fn move_file( _: Claims, file_system: web::Data, @@ -1256,6 +1327,10 @@ mod tests { let count = file_paths.len() as i64; Ok((file_paths.to_vec(), count)) } + + fn get_all_with_gps(&mut self, context: &opentelemetry::Context, base_path: &str, recursive: bool) -> Result)>, DbError> { + todo!() + } } mod api { diff --git a/src/main.rs b/src/main.rs index c2b3161..34a7bf2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -853,6 +853,10 @@ fn main() -> std::io::Result<()> { web::resource("/photos") .route(web::get().to(files::list_photos::)), ) + .service( + web::resource("/photos/gps-summary") + .route(web::get().to(files::get_gps_summary)), + ) .service(web::resource("/file/move").post(move_file::)) .service(get_image) .service(upload_image) From 1d2f4e3441a37c77d99c4f539072a7bbba51d95b Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 26 Jan 2026 20:04:14 -0500 Subject: [PATCH 2/5] Add circular thumbnail creation for Map view --- src/data/mod.rs | 10 +++++++ src/files.rs | 1 - src/main.rs | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/data/mod.rs b/src/data/mod.rs index 53e7020..8303c6a 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -186,6 +186,8 @@ pub struct ThumbnailRequest { #[serde(default)] #[allow(dead_code)] // Part of API contract, may be used in future pub(crate) format: Option, + #[serde(default)] + pub(crate) shape: Option, } #[derive(Debug, Deserialize, PartialEq)] @@ -196,6 +198,14 @@ pub enum ThumbnailFormat { Image, } +#[derive(Debug, Deserialize, PartialEq)] +pub enum ThumbnailShape { + #[serde(rename = "circle")] + Circle, + #[serde(rename = "square")] + Square, +} + #[derive(Deserialize)] pub struct LoginRequest { pub username: String, diff --git a/src/files.rs b/src/files.rs index d925742..ce4f9db 100644 --- a/src/files.rs +++ b/src/files.rs @@ -957,7 +957,6 @@ pub async fn get_gps_summary( _: Claims, request: HttpRequest, req: web::Query, - state: web::Data, exif_dao: web::Data>>, ) -> Result { use crate::data::{GpsPhotoSummary, GpsPhotosResponse}; diff --git a/src/main.rs b/src/main.rs index 34a7bf2..4b65dea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -116,6 +116,26 @@ async fn get_image( thumb_path.set_extension("gif"); } + // Handle circular thumbnail request + if req.shape == Some(ThumbnailShape::Circle) { + match create_circular_thumbnail(&thumb_path, &thumbs).await { + Ok(circular_path) => { + if let Ok(file) = NamedFile::open(&circular_path) { + span.set_status(Status::Ok); + return file + .use_etag(true) + .use_last_modified(true) + .prefer_utf8(true) + .into_response(&request); + } + } + Err(e) => { + warn!("Failed to create circular thumbnail: {:?}", e); + // Fall through to serve square thumbnail + } + } + } + trace!("Thumbnail path: {:?}", thumb_path); if let Ok(file) = NamedFile::open(&thumb_path) { span.set_status(Status::Ok); @@ -153,6 +173,65 @@ fn is_video_file(path: &Path) -> bool { file_types::is_video_file(path) } +async fn create_circular_thumbnail(thumb_path: &Path, thumbs_dir: &str) -> Result> { + use image::{ImageBuffer, Rgba, GenericImageView}; + + // Create circular thumbnails directory + let circular_dir = Path::new(thumbs_dir).join("_circular"); + + // Get relative path from thumbs_dir to create same structure + let relative_to_thumbs = thumb_path.strip_prefix(thumbs_dir)?; + let circular_path = circular_dir.join(relative_to_thumbs).with_extension("png"); + + // Check if circular thumbnail already exists + if circular_path.exists() { + return Ok(circular_path); + } + + // Create parent directory if needed + if let Some(parent) = circular_path.parent() { + std::fs::create_dir_all(parent)?; + } + + // Load the square thumbnail + let img = image::open(thumb_path)?; + let (width, height) = img.dimensions(); + + // Fixed output size for consistency + let output_size = 80u32; + let radius = output_size as f32 / 2.0; + + // Calculate crop area to get square center of original image + let crop_size = width.min(height); + let crop_x = (width - crop_size) / 2; + let crop_y = (height - crop_size) / 2; + + // Create a new RGBA image with transparency + let output = ImageBuffer::from_fn(output_size, output_size, |x, y| { + let dx = x as f32 - radius; + let dy = y as f32 - radius; + let distance = (dx * dx + dy * dy).sqrt(); + + if distance <= radius { + // Inside circle - map to cropped source area + // Scale from output coordinates to crop coordinates + let scale = crop_size as f32 / output_size as f32; + let src_x = crop_x + (x as f32 * scale) as u32; + let src_y = crop_y + (y as f32 * scale) as u32; + let pixel = img.get_pixel(src_x, src_y); + Rgba([pixel[0], pixel[1], pixel[2], 255]) + } else { + // Outside circle - transparent + Rgba([0, 0, 0, 0]) + } + }); + + // Save as PNG (supports transparency) + output.save(&circular_path)?; + + Ok(circular_path) +} + #[get("/image/metadata")] async fn get_file_metadata( _: Claims, From a6cc64ece01de389d8f81da79003709e775748f3 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 26 Jan 2026 20:05:42 -0500 Subject: [PATCH 3/5] Bump to version 0.5.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a17c92..3e1e750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1797,7 +1797,7 @@ dependencies = [ [[package]] name = "image-api" -version = "0.5.1" +version = "0.5.2" dependencies = [ "actix", "actix-cors", diff --git a/Cargo.toml b/Cargo.toml index 2c237b9..88b9f09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "image-api" -version = "0.5.1" +version = "0.5.2" authors = ["Cameron Cordes "] edition = "2024" From 7d2a3148bbd72ab88533bc9231093e5af0081048 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 26 Jan 2026 20:51:11 -0500 Subject: [PATCH 4/5] Make Memories week span sorting chronological --- src/memories.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/memories.rs b/src/memories.rs index f120d36..c3754d3 100644 --- a/src/memories.rs +++ b/src/memories.rs @@ -567,13 +567,13 @@ pub async fn list_memories( match span_mode { // Sort by absolute time for a more 'overview' MemoriesSpan::Month => memories_with_dates.sort_by(|a, b| a.1.cmp(&b.1)), - // For week span, sort by day of month, then by full timestamp (oldest first) + // For week span, sort by full date + timestamp (chronological) MemoriesSpan::Week => { memories_with_dates.sort_by(|a, b| { - // First, sort by day of month - let day_cmp = a.1.day().cmp(&b.1.day()); - if day_cmp != std::cmp::Ordering::Equal { - return day_cmp; + // First, sort by full date (year, month, day) + let date_cmp = a.1.cmp(&b.1); + if date_cmp != std::cmp::Ordering::Equal { + return date_cmp; } // Then sort by full created timestamp (oldest to newest) From 1efdd02eda8ee95e7bdc271c39f42cff66ab39c3 Mon Sep 17 00:00:00 2001 From: Cameron Date: Wed, 28 Jan 2026 10:52:17 -0500 Subject: [PATCH 5/5] Add GPS summary sorting Run cargo fmt/clippy --- src/database/mod.rs | 10 +++- src/files.rs | 129 +++++++++++++++++++++++++++++--------------- src/main.rs | 59 ++++++++++++-------- src/video/actors.rs | 12 +++-- 4 files changed, 138 insertions(+), 72 deletions(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index 7d058d8..abec436 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -710,8 +710,14 @@ impl ExifDao for SqliteExifDao { let filtered: Vec<(String, f64, f64, Option)> = results .into_iter() .filter_map(|exif| { - if let (Some(lat_val), Some(lon_val)) = (exif.gps_latitude, exif.gps_longitude) { - Some((exif.file_path, lat_val as f64, lon_val as f64, exif.date_taken)) + if let (Some(lat_val), Some(lon_val)) = (exif.gps_latitude, exif.gps_longitude) + { + Some(( + exif.file_path, + lat_val as f64, + lon_val as f64, + exif.date_taken, + )) } else { None } diff --git a/src/files.rs b/src/files.rs index ce4f9db..264f796 100644 --- a/src/files.rs +++ b/src/files.rs @@ -187,10 +187,10 @@ pub async fn list_photos( _: Claims, request: HttpRequest, req: Query, - app_state: web::Data, - file_system: web::Data, - tag_dao: web::Data>, - exif_dao: web::Data>>, + app_state: Data, + file_system: Data, + tag_dao: Data>, + exif_dao: Data>>, ) -> HttpResponse { let search_path = &req.path; @@ -756,7 +756,7 @@ pub async fn list_photos( fn sort(mut files: Vec, sort_type: SortType) -> Vec { match sort_type { SortType::Shuffle => files.shuffle(&mut thread_rng()), - SortType::NameAsc => { + NameAsc => { files.sort_by(|l, r| l.file_name.cmp(&r.file_name)); } SortType::NameDesc => { @@ -786,7 +786,7 @@ fn sort(mut files: Vec, sort_type: SortType) -> Vec { fn sort_with_metadata(mut files: Vec, sort_type: SortType) -> Vec { match sort_type { SortType::Shuffle => files.shuffle(&mut thread_rng()), - SortType::NameAsc => { + NameAsc => { files.sort_by(|l, r| l.file_name.cmp(&r.file_name)); } SortType::NameDesc => { @@ -956,8 +956,8 @@ fn is_path_above_base_dir + Debug>( pub async fn get_gps_summary( _: Claims, request: HttpRequest, - req: web::Query, - exif_dao: web::Data>>, + req: Query, + exif_dao: Data>>, ) -> Result { use crate::data::{GpsPhotoSummary, GpsPhotosResponse}; @@ -968,7 +968,10 @@ pub async fn get_gps_summary( .start_with_context(&tracer, &parent_cx); span.set_attribute(KeyValue::new("path", req.path.clone())); - span.set_attribute(KeyValue::new("recursive", req.recursive.unwrap_or(false).to_string())); + span.set_attribute(KeyValue::new( + "recursive", + req.recursive.unwrap_or(false).to_string(), + )); let cx = opentelemetry::Context::current_with_span(span); @@ -989,41 +992,76 @@ pub async fn get_gps_summary( }; let recursive = req.recursive.unwrap_or(false); - info!("Fetching GPS photos for path='{}' recursive={}", requested_path, recursive); + info!( + "Fetching GPS photos for path='{}' recursive={}", + requested_path, recursive + ); // Query database for all photos with GPS let mut exif_dao_guard = exif_dao.lock().expect("Unable to get ExifDao"); match exif_dao_guard.get_all_with_gps(&cx, requested_path, recursive) { - Ok(gps_data) => { - let photos: Vec = gps_data - .into_iter() - .map(|(path, lat, lon, date_taken)| GpsPhotoSummary { - path, - lat, - lon, - date_taken, - }) - .collect(); + Ok(gps_data) => { + let mut photos: Vec = gps_data + .into_iter() + .map(|(path, lat, lon, date_taken)| GpsPhotoSummary { + path, + lat, + lon, + date_taken, + }) + .collect(); - let total = photos.len(); - cx.span().set_attribute(KeyValue::new("result_count", total as i64)); - cx.span().set_status(Status::Ok); + // Sort by date_taken based on request, defaulting to ascending (oldest to newest) + use crate::data::SortType; + let sort_type = req.sort.unwrap_or(SortType::DateTakenAsc); - Ok(HttpResponse::Ok().json(GpsPhotosResponse { photos, total })) - } - Err(e) => { - error!("Error querying GPS data: {:?}", e); - cx.span().set_status(Status::error(format!("Database error: {:?}", e))); - Ok(HttpResponse::InternalServerError().json(serde_json::json!({ - "error": "Failed to query GPS data" - }))) + match sort_type { + SortType::DateTakenDesc => { + photos.sort_by(|a, b| match (a.date_taken, b.date_taken) { + (Some(date_a), Some(date_b)) => date_b.cmp(&date_a), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => a.path.cmp(&b.path), + }); + } + NameAsc => { + photos.sort_by(|a, b| a.path.cmp(&b.path)); + } + SortType::NameDesc => { + photos.sort_by(|a, b| b.path.cmp(&a.path)); + } + _ => { + // Default: DateTakenAsc + photos.sort_by(|a, b| match (a.date_taken, b.date_taken) { + (Some(date_a), Some(date_b)) => date_a.cmp(&date_b), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => a.path.cmp(&b.path), + }); + } } + + let total = photos.len(); + cx.span() + .set_attribute(KeyValue::new("result_count", total as i64)); + cx.span().set_status(Status::Ok); + + Ok(HttpResponse::Ok().json(GpsPhotosResponse { photos, total })) } + Err(e) => { + error!("Error querying GPS data: {:?}", e); + cx.span() + .set_status(Status::error(format!("Database error: {:?}", e))); + Ok(HttpResponse::InternalServerError().json(serde_json::json!({ + "error": "Failed to query GPS data" + }))) + } + } } pub async fn move_file( _: Claims, - file_system: web::Data, + file_system: Data, app_state: Data, request: web::Json, ) -> HttpResponse { @@ -1223,7 +1261,7 @@ mod tests { &mut self, _context: &opentelemetry::Context, _: &str, - ) -> Result, crate::database::DbError> { + ) -> Result, DbError> { Ok(None) } @@ -1231,7 +1269,7 @@ mod tests { &mut self, _context: &opentelemetry::Context, data: crate::database::models::InsertImageExif, - ) -> Result { + ) -> Result { // Return a dummy ImageExif for tests Ok(crate::database::models::ImageExif { id: 1, @@ -1259,14 +1297,14 @@ mod tests { &mut self, _context: &opentelemetry::Context, _: &str, - ) -> Result<(), crate::database::DbError> { + ) -> Result<(), DbError> { Ok(()) } fn get_all_with_date_taken( &mut self, _context: &opentelemetry::Context, - ) -> Result, crate::database::DbError> { + ) -> Result, DbError> { Ok(Vec::new()) } @@ -1274,7 +1312,7 @@ mod tests { &mut self, _context: &opentelemetry::Context, _: &[String], - ) -> Result, crate::database::DbError> { + ) -> Result, DbError> { Ok(Vec::new()) } @@ -1287,14 +1325,14 @@ mod tests { _: Option<(f64, f64, f64, f64)>, _: Option, _: Option, - ) -> Result, crate::database::DbError> { + ) -> Result, DbError> { Ok(Vec::new()) } fn get_camera_makes( &mut self, _context: &opentelemetry::Context, - ) -> Result, crate::database::DbError> { + ) -> Result, DbError> { Ok(Vec::new()) } @@ -1327,7 +1365,12 @@ mod tests { Ok((file_paths.to_vec(), count)) } - fn get_all_with_gps(&mut self, context: &opentelemetry::Context, base_path: &str, recursive: bool) -> Result)>, DbError> { + fn get_all_with_gps( + &mut self, + _context: &opentelemetry::Context, + _base_path: &str, + _recursive: bool, + ) -> Result)>, DbError> { todo!() } } @@ -1429,7 +1472,7 @@ mod tests { Data::new(RealFileSystem::new(temp_dir.to_str().unwrap().to_string())), Data::new(Mutex::new(SqliteTagDao::default())), Data::new(Mutex::new( - Box::new(MockExifDao) as Box + Box::new(MockExifDao) as Box )), ) .await; @@ -1476,7 +1519,7 @@ mod tests { Data::new(FakeFileSystem::new(HashMap::new())), Data::new(Mutex::new(tag_dao)), Data::new(Mutex::new( - Box::new(MockExifDao) as Box + Box::new(MockExifDao) as Box )), ) .await; @@ -1539,7 +1582,7 @@ mod tests { Data::new(FakeFileSystem::new(HashMap::new())), Data::new(Mutex::new(tag_dao)), Data::new(Mutex::new( - Box::new(MockExifDao) as Box + Box::new(MockExifDao) as Box )), ) .await; diff --git a/src/main.rs b/src/main.rs index 4b65dea..529b97d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -118,7 +118,7 @@ async fn get_image( // Handle circular thumbnail request if req.shape == Some(ThumbnailShape::Circle) { - match create_circular_thumbnail(&thumb_path, &thumbs).await { + match create_circular_thumbnail(&thumb_path, thumbs).await { Ok(circular_path) => { if let Ok(file) = NamedFile::open(&circular_path) { span.set_status(Status::Ok); @@ -173,8 +173,11 @@ fn is_video_file(path: &Path) -> bool { file_types::is_video_file(path) } -async fn create_circular_thumbnail(thumb_path: &Path, thumbs_dir: &str) -> Result> { - use image::{ImageBuffer, Rgba, GenericImageView}; +async fn create_circular_thumbnail( + thumb_path: &Path, + thumbs_dir: &str, +) -> Result> { + use image::{GenericImageView, ImageBuffer, Rgba}; // Create circular thumbnails directory let circular_dir = Path::new(thumbs_dir).join("_circular"); @@ -1040,12 +1043,11 @@ fn cleanup_orphaned_playlists() { .filter_map(|e| e.ok()) .filter(|e| e.file_type().is_file()) { - if let Some(entry_stem) = entry.path().file_stem() { - if entry_stem == filename && is_video_file(entry.path()) { + if let Some(entry_stem) = entry.path().file_stem() + && entry_stem == filename && is_video_file(entry.path()) { video_exists = true; break; } - } } if !video_exists { @@ -1056,7 +1058,11 @@ fn cleanup_orphaned_playlists() { // Delete the playlist file if let Err(e) = std::fs::remove_file(&playlist_path) { - warn!("Failed to delete playlist {}: {}", playlist_path.display(), e); + warn!( + "Failed to delete playlist {}: {}", + playlist_path.display(), + e + ); error_count += 1; } else { deleted_count += 1; @@ -1071,13 +1077,14 @@ fn cleanup_orphaned_playlists() { .filter(|e| e.file_type().is_file()) { let entry_path = entry.path(); - if let Some(ext) = entry_path.extension() { - if ext.eq_ignore_ascii_case("ts") { + if let Some(ext) = entry_path.extension() + && ext.eq_ignore_ascii_case("ts") { // Check if this .ts file belongs to our playlist if let Some(ts_stem) = entry_path.file_stem() { let ts_name = ts_stem.to_string_lossy(); if ts_name.starts_with(&*video_filename) { - if let Err(e) = std::fs::remove_file(entry_path) { + if let Err(e) = std::fs::remove_file(entry_path) + { debug!( "Failed to delete segment {}: {}", entry_path.display(), @@ -1092,7 +1099,6 @@ fn cleanup_orphaned_playlists() { } } } - } } } } @@ -1154,7 +1160,12 @@ fn watch_files(playlist_manager: Addr) { if is_full_scan { info!("Running full scan (scan #{})", scan_count); - process_new_files(&base_path, Arc::clone(&exif_dao), None, playlist_manager.clone()); + process_new_files( + &base_path, + Arc::clone(&exif_dao), + None, + playlist_manager.clone(), + ); last_full_scan = now; } else { debug!( @@ -1165,7 +1176,12 @@ fn watch_files(playlist_manager: Addr) { let check_since = last_quick_scan .checked_sub(Duration::from_secs(10)) .unwrap_or(last_quick_scan); - process_new_files(&base_path, Arc::clone(&exif_dao), Some(check_since), playlist_manager.clone()); + process_new_files( + &base_path, + Arc::clone(&exif_dao), + Some(check_since), + playlist_manager.clone(), + ); } last_quick_scan = now; @@ -1190,13 +1206,12 @@ fn playlist_needs_generation(video_path: &Path, playlist_path: &Path) -> bool { if let (Ok(video_meta), Ok(playlist_meta)) = ( std::fs::metadata(video_path), std::fs::metadata(playlist_path), - ) { - if let (Ok(video_modified), Ok(playlist_modified)) = + ) + && let (Ok(video_modified), Ok(playlist_modified)) = (video_meta.modified(), playlist_meta.modified()) { return video_modified > playlist_modified; } - } // If we can't determine, assume it needs generation true @@ -1276,7 +1291,7 @@ fn process_new_files( let needs_thumbnail = !thumb_path.exists(); // Check if EXIF data exists (for supported files) - let needs_exif = if exif::supports_exif(&file_path) { + let needs_exif = if exif::supports_exif(file_path) { !existing_exif_paths.contains_key(relative_path) } else { false @@ -1349,16 +1364,14 @@ fn process_new_files( let mut videos_needing_playlists = Vec::new(); for (file_path, _relative_path) in &files { - if is_video_file(&file_path) { + if is_video_file(file_path) { // Construct expected playlist path - let playlist_filename = format!( - "{}.m3u8", - file_path.file_name().unwrap().to_string_lossy() - ); + let playlist_filename = + format!("{}.m3u8", file_path.file_name().unwrap().to_string_lossy()); let playlist_path = Path::new(&video_path_base).join(&playlist_filename); // Check if playlist needs (re)generation - if playlist_needs_generation(&file_path, &playlist_path) { + if playlist_needs_generation(file_path, &playlist_path) { videos_needing_playlists.push(file_path.clone()); } } diff --git a/src/video/actors.rs b/src/video/actors.rs index a6c59e9..66e0341 100644 --- a/src/video/actors.rs +++ b/src/video/actors.rs @@ -384,10 +384,14 @@ impl Handler for PlaylistGenerator { let use_copy = is_h264 && !has_rotation; if has_rotation { - info!("Video {} has rotation metadata ({}°), transcoding to apply rotation", video_file, rotation); - span.add_event("Transcoding due to rotation", vec![ - KeyValue::new("rotation_degrees", rotation as i64) - ]); + info!( + "Video {} has rotation metadata ({}°), transcoding to apply rotation", + video_file, rotation + ); + span.add_event( + "Transcoding due to rotation", + vec![KeyValue::new("rotation_degrees", rotation as i64)], + ); } else if use_copy { info!("Video {} is already h264, using stream copy", video_file); span.add_event("Using stream copy (h264 detected)", vec![]);