feature/exif-endpoint #44

Merged
cameron merged 29 commits from feature/exif-endpoint into master 2025-12-27 03:25:19 +00:00
Showing only changes of commit ccd16ba987 - Show all commits

View File

@@ -43,6 +43,56 @@ pub struct FileWithMetadata {
} }
use serde::Deserialize; use serde::Deserialize;
/// Apply sorting to files with EXIF data support for date-based sorting
/// Handles both date sorting (with EXIF/filename fallback) and regular sorting
fn apply_sorting_with_exif(
files: Vec<FileWithTagCount>,
sort_type: SortType,
exif_dao: &mut Box<dyn ExifDao>,
span_context: &opentelemetry::Context,
) -> Vec<String> {
match sort_type {
SortType::DateTakenAsc | SortType::DateTakenDesc => {
info!("Date sorting requested, fetching EXIF data");
// Collect file paths for batch EXIF query
let file_paths: Vec<String> = files.iter().map(|f| f.file_name.clone()).collect();
// Batch fetch EXIF data
let exif_map: std::collections::HashMap<String, i64> = exif_dao
.get_exif_batch(span_context, &file_paths)
.unwrap_or_default()
.into_iter()
.filter_map(|exif| exif.date_taken.map(|dt| (exif.file_path, dt)))
.collect();
// Convert to FileWithMetadata with date fallback logic
let files_with_metadata: Vec<FileWithMetadata> = files
.into_iter()
.map(|f| {
// Try EXIF date first
let date_taken = exif_map.get(&f.file_name).copied().or_else(|| {
// Fallback to filename extraction
extract_date_from_filename(&f.file_name).map(|dt| dt.timestamp())
});
FileWithMetadata {
file_name: f.file_name,
tag_count: f.tag_count,
date_taken,
}
})
.collect();
sort_with_metadata(files_with_metadata, sort_type)
}
_ => {
// Use regular sort for non-date sorting
sort(files, sort_type)
}
}
}
pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>( pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
_: Claims, _: Claims,
request: HttpRequest, request: HttpRequest,
@@ -264,51 +314,13 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
.collect::<Vec<FileWithTagCount>>() .collect::<Vec<FileWithTagCount>>()
}) })
.map(|files| { .map(|files| {
// Handle sorting - use FileWithMetadata for date sorting to support EXIF dates // Handle sorting - use helper function that supports EXIF date sorting
let sort_type = req.sort.unwrap_or(NameAsc); let sort_type = req.sort.unwrap_or(NameAsc);
match sort_type {
SortType::DateTakenAsc | SortType::DateTakenDesc => {
info!("Date sorting requested in tagged/recursive search, fetching EXIF data");
// Collect file paths for batch EXIF query
let file_paths: Vec<String> =
files.iter().map(|f| f.file_name.clone()).collect();
// Batch fetch EXIF data
let mut exif_dao_guard = exif_dao.lock().expect("Unable to get ExifDao"); let mut exif_dao_guard = exif_dao.lock().expect("Unable to get ExifDao");
let exif_map: std::collections::HashMap<String, i64> = exif_dao_guard let result =
.get_exif_batch(&span_context, &file_paths) apply_sorting_with_exif(files, sort_type, &mut exif_dao_guard, &span_context);
.unwrap_or_default()
.into_iter()
.filter_map(|exif| exif.date_taken.map(|dt| (exif.file_path, dt)))
.collect();
drop(exif_dao_guard); drop(exif_dao_guard);
result
// Convert to FileWithMetadata with date fallback logic
let files_with_metadata: Vec<FileWithMetadata> = files
.into_iter()
.map(|f| {
// Try EXIF date first
let date_taken = exif_map.get(&f.file_name).copied().or_else(|| {
// Fallback to filename extraction
extract_date_from_filename(&f.file_name).map(|dt| dt.timestamp())
});
FileWithMetadata {
file_name: f.file_name,
tag_count: f.tag_count,
date_taken,
}
})
.collect();
sort_with_metadata(files_with_metadata, sort_type)
}
_ => {
// Use regular sort for non-date sorting
sort(files, sort_type)
}
}
}) })
.inspect(|files| debug!("Found {:?} files", files.len())) .inspect(|files| debug!("Found {:?} files", files.len()))
.map(|tagged_files: Vec<String>| { .map(|tagged_files: Vec<String>| {
@@ -439,53 +451,13 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
info!("After all filters, {} files remain", photos.len()); info!("After all filters, {} files remain", photos.len());
// Handle sorting - use FileWithMetadata for date sorting to support EXIF dates // Handle sorting - use helper function that supports EXIF date sorting
let response_files = if let Some(sort_type) = req.sort { let response_files = if let Some(sort_type) = req.sort {
match sort_type {
SortType::DateTakenAsc | SortType::DateTakenDesc => {
info!("Date sorting requested, fetching EXIF data");
// Collect file paths for batch EXIF query
let file_paths: Vec<String> =
photos.iter().map(|f| f.file_name.clone()).collect();
// Batch fetch EXIF data
let mut exif_dao_guard = exif_dao.lock().expect("Unable to get ExifDao"); let mut exif_dao_guard = exif_dao.lock().expect("Unable to get ExifDao");
let exif_map: std::collections::HashMap<String, i64> = exif_dao_guard let result =
.get_exif_batch(&span_context, &file_paths) apply_sorting_with_exif(photos, sort_type, &mut exif_dao_guard, &span_context);
.unwrap_or_default()
.into_iter()
.filter_map(|exif| exif.date_taken.map(|dt| (exif.file_path, dt)))
.collect();
drop(exif_dao_guard); drop(exif_dao_guard);
result
// Convert to FileWithMetadata with date fallback logic
let files_with_metadata: Vec<FileWithMetadata> = photos
.into_iter()
.map(|f| {
// Try EXIF date first
let date_taken =
exif_map.get(&f.file_name).copied().or_else(|| {
// Fallback to filename extraction
extract_date_from_filename(&f.file_name)
.map(|dt| dt.timestamp())
});
FileWithMetadata {
file_name: f.file_name,
tag_count: f.tag_count,
date_taken,
}
})
.collect();
sort_with_metadata(files_with_metadata, sort_type)
}
_ => {
// Use regular sort for non-date sorting
sort(photos, sort_type)
}
}
} else { } else {
// No sorting requested // No sorting requested
photos photos
@@ -540,9 +512,9 @@ fn sort(mut files: Vec<FileWithTagCount>, sort_type: SortType) -> Vec<String> {
files.sort_by(|l, r| r.tag_count.cmp(&l.tag_count)); files.sort_by(|l, r| r.tag_count.cmp(&l.tag_count));
} }
SortType::DateTakenAsc | SortType::DateTakenDesc => { SortType::DateTakenAsc | SortType::DateTakenDesc => {
// Date sorting not yet implemented for FileWithTagCount // Date sorting not implemented for FileWithTagCount
// Will be implemented when integrating with FileWithMetadata // We shouldn't be hitting this code
// For now, fall back to name sorting warn!("Date sorting not implemented for FileWithTagCount");
files.sort_by(|l, r| l.file_name.cmp(&r.file_name)); files.sort_by(|l, r| l.file_name.cmp(&r.file_name));
} }
} }
@@ -569,26 +541,22 @@ fn sort_with_metadata(mut files: Vec<FileWithMetadata>, sort_type: SortType) ->
SortType::TagCountDesc => { SortType::TagCountDesc => {
files.sort_by(|l, r| r.tag_count.cmp(&l.tag_count)); files.sort_by(|l, r| r.tag_count.cmp(&l.tag_count));
} }
SortType::DateTakenAsc => { SortType::DateTakenAsc | SortType::DateTakenDesc => {
files.sort_by(|l, r| { files.sort_by(|l, r| {
match (l.date_taken, r.date_taken) { match (l.date_taken, r.date_taken) {
(Some(a), Some(b)) => a.cmp(&b), (Some(a), Some(b)) => {
if sort_type == SortType::DateTakenAsc {
a.cmp(&b)
} else {
b.cmp(&a)
}
}
(Some(_), None) => std::cmp::Ordering::Less, // Dated photos first (Some(_), None) => std::cmp::Ordering::Less, // Dated photos first
(None, Some(_)) => std::cmp::Ordering::Greater, (None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => l.file_name.cmp(&r.file_name), // Fallback to name (None, None) => l.file_name.cmp(&r.file_name), // Fallback to name
} }
}); });
} }
SortType::DateTakenDesc => {
files.sort_by(|l, r| {
match (l.date_taken, r.date_taken) {
(Some(a), Some(b)) => b.cmp(&a), // Reverse for descending
(Some(_), None) => std::cmp::Ordering::Less, // Dated photos first
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => r.file_name.cmp(&l.file_name), // Fallback reversed
}
});
}
} }
files files