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![]);