use exif::Error as _ExifError;
use log::debug;
use log::info;
use log::warn;
use crate::errors::ClineupError;
use crate::exif_extractor::ExifExtractor;
use crate::gps::base::GpsResolutionProvider;
use crate::gps::location::LocationInfo;
use crate::placeholders::Placeholder;
use crate::utils::is_there_a_exif_placeholder;
use crate::utils::is_there_a_location_placeholder;
use crate::utils::is_there_a_metadata_placeholder;
use chrono::prelude::{DateTime, Local};
use indexmap::IndexMap;
use std::path::Path;
use std::path::PathBuf;
pub fn get_fallback_name(which: &str) -> String {
format!("Unknown {}", which)
}
macro_rules! handle_placeholder {
($provider:expr, $placeholder:expr, $fallback_name:expr, $formatter:expr, $is_fallback:ident) => {
match $provider
.as_ref()
.ok_or_else(|| ClineupError::InvalidPlaceholderMapping($placeholder.to_string()))?
{
Ok(v) => match $formatter(v) {
Ok(result) => {
debug!("Found {} for placeholder {}", result, $placeholder);
result
}
Err(err) => {
warn!("{}", err);
$is_fallback = true;
get_fallback_name($fallback_name)
}
},
Err(err) => {
warn!("{}", err);
$is_fallback = true;
get_fallback_name($fallback_name)
}
}
};
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
struct StringLatLon(String, String);
fn round_float_to_nth_decimal_place(num: f32, decimal_places: u32) -> f32 {
let multiplier = 10_f32.powi(decimal_places as i32);
(num * multiplier).round() / multiplier
}
pub struct PathFormatter<'a, 'b> {
path_to_format: &'a String,
placeholders: &'b IndexMap<String, IndexMap<String, Placeholder>>,
reverse_geocoding: Option<Box<dyn GpsResolutionProvider>>,
gps_positions: IndexMap<StringLatLon, LocationInfo>,
optimize_gps: bool,
}
impl<'a, 'b> PathFormatter<'a, 'b> {
pub fn new(
path_to_format: &'a String,
placeholders: &'b IndexMap<String, IndexMap<String, Placeholder>>,
reverse_geocoding: Option<Box<dyn GpsResolutionProvider>>,
optimize_gps: bool,
) -> Self {
let gps_positions = IndexMap::new();
PathFormatter {
path_to_format,
placeholders,
reverse_geocoding,
gps_positions,
optimize_gps,
}
}
fn get_location_info(
&mut self,
exif_extractor: &ExifExtractor,
) -> Result<LocationInfo, ClineupError> {
let lat = exif_extractor.get_latitude()?;
let lon = exif_extractor.get_longitude()?;
if !self.optimize_gps {
return self
.reverse_geocoding
.as_ref()
.unwrap()
.get_location(lat, lon);
}
let rounded_lat = round_float_to_nth_decimal_place(lat, 1);
let rounded_lon = round_float_to_nth_decimal_place(lon, 1);
let string_lat_lon = StringLatLon(rounded_lat.to_string(), rounded_lon.to_string());
if self.gps_positions.contains_key(&string_lat_lon) {
debug!("Get already computed location {:?}", string_lat_lon);
return Ok(self.gps_positions.get(&string_lat_lon).unwrap().clone());
}
let location = self
.reverse_geocoding
.as_ref()
.unwrap()
.get_location(rounded_lat, rounded_lon);
match location {
Ok(v) => {
debug!("Store location {:?}", v);
self.gps_positions.insert(string_lat_lon, v.clone());
Ok(v)
}
Err(_) => Err(ClineupError::LatOrLonMissing),
}
}
fn get_file_metadata(&self, path: &PathBuf) -> Result<std::fs::Metadata, ClineupError> {
if is_there_a_metadata_placeholder(self.placeholders) {
std::fs::metadata(path).map_err(ClineupError::from)
} else {
Err(ClineupError::NoLocationPlaceholderFound)
}
}
}
impl<'a, 'b> PathFormatter<'a, 'b> {
pub fn get_formatted_path(&mut self, path: &PathBuf) -> Result<PathBuf, ClineupError> {
let mut formatted_path = self.path_to_format.clone();
let file_metadata = if is_there_a_metadata_placeholder(self.placeholders) {
Some(self.get_file_metadata(path))
} else {
None
};
let exif_extractor = if is_there_a_exif_placeholder(self.placeholders) {
Some(ExifExtractor::new(path))
} else {
None
};
let location = if is_there_a_location_placeholder(self.placeholders) {
if let Some(result) = &exif_extractor {
match result {
Ok(v) => Some(self.get_location_info(v)),
Err(_) => Some(Err(ClineupError::ExifError {
source: _ExifError::NotFound("No exif data found"),
file: path.to_string_lossy().to_string(),
})),
}
} else {
None
}
} else {
None
};
for (full_text, placeholders) in self.placeholders {
let mut result = String::new();
let mut is_fallback = false;
for (placeholder_text, placeholder) in placeholders {
debug!("Compute placeholder {:?}", full_text);
let current_result = match placeholder {
Placeholder::Year => {
handle_placeholder!(
exif_extractor,
"Year",
"Year",
|v: &ExifExtractor| {
v.get_exif_date().map(|date| date.format("%Y").to_string())
},
is_fallback
)
}
Placeholder::Month => {
handle_placeholder!(
exif_extractor,
"Month",
"Month",
|v: &ExifExtractor| {
v.get_exif_date().map(|date| date.format("%m").to_string())
},
is_fallback
)
}
Placeholder::Day => {
handle_placeholder!(
exif_extractor,
"Day",
"Day",
|v: &ExifExtractor| {
v.get_exif_date().map(|date| date.format("%d").to_string())
},
is_fallback
)
}
Placeholder::Width => {
handle_placeholder!(
exif_extractor,
"Width",
"Width",
|v: &ExifExtractor| { v.get_width().map(|width| width.to_string()) },
is_fallback
)
}
Placeholder::Height => {
handle_placeholder!(
exif_extractor,
"Height",
"Height",
|v: &ExifExtractor| { v.get_height().map(|height| height.to_string()) },
is_fallback
)
}
Placeholder::CameraModel => {
handle_placeholder!(
exif_extractor,
"Camera Model",
"Camera Model",
|v: &ExifExtractor| {
v.get_camera_model().map(|model| model.to_string())
},
is_fallback
)
}
Placeholder::CameraBrand => {
handle_placeholder!(
exif_extractor,
"Camera Brand",
"Camera Brand",
|v: &ExifExtractor| {
v.get_camera_brand().map(|brand| brand.to_string())
},
is_fallback
)
}
Placeholder::CTimeYear => handle_placeholder!(
file_metadata.as_ref(),
"CTimeYear",
"Creation Time Year",
|v: &std::fs::Metadata| {
v.created()
.map(|date| DateTime::<Local>::from(date).format("%Y").to_string())
},
is_fallback
),
Placeholder::CTimeMonth => handle_placeholder!(
file_metadata.as_ref(),
"CTimeMonth",
"Creation Time Month",
|v: &std::fs::Metadata| {
v.clone()
.created()
.map(|date| DateTime::<Local>::from(date).format("%m").to_string())
},
is_fallback
),
Placeholder::CTimeDay => handle_placeholder!(
file_metadata.as_ref(),
"CTimeDay",
"Creation Time Day",
|v: &std::fs::Metadata| {
v.clone()
.created()
.map(|date| DateTime::<Local>::from(date).format("%d").to_string())
},
is_fallback
),
Placeholder::MTimeYear => handle_placeholder!(
file_metadata.as_ref(),
"MTimeYear",
"Modification Time Year",
|v: &std::fs::Metadata| {
v.modified()
.map(|date| DateTime::<Local>::from(date).format("%Y").to_string())
},
is_fallback
),
Placeholder::MTimeMonth => handle_placeholder!(
file_metadata.as_ref(),
"MTimeMonth",
"Modification Time Month",
|v: &std::fs::Metadata| {
v.modified()
.map(|date| DateTime::<Local>::from(date).format("%m").to_string())
},
is_fallback
),
Placeholder::MTimeDay => handle_placeholder!(
file_metadata.as_ref(),
"MTimeDay",
"Modification Time Day",
|v: &std::fs::Metadata| {
v.modified()
.map(|date| DateTime::<Local>::from(date).format("%d").to_string())
},
is_fallback
),
Placeholder::Country => handle_placeholder!(
location.as_ref(),
"Country",
"Country",
|v: &LocationInfo| {
v.country()
.ok_or(ClineupError::MissingLocation("Country".to_string()))
.map(|v| v.to_string())
},
is_fallback
),
Placeholder::State => handle_placeholder!(
location.as_ref(),
"State",
"State",
|v: &LocationInfo| {
v.state()
.ok_or(ClineupError::MissingLocation("Country".to_string()))
.map(|v| v.to_string())
},
is_fallback
),
Placeholder::City => handle_placeholder!(
location.as_ref(),
"City",
"City",
|v: &LocationInfo| {
v.city()
.ok_or(ClineupError::MissingLocation("City".to_string()))
.map(|v| v.to_string())
},
is_fallback
),
Placeholder::County => handle_placeholder!(
location.as_ref(),
"County",
"County",
|v: &LocationInfo| {
v.county()
.ok_or(ClineupError::MissingLocation("County".to_string()))
.map(|v| v.to_string())
},
is_fallback
),
Placeholder::Municipality => handle_placeholder!(
location.as_ref(),
"Municipality",
"Municipality",
|v: &LocationInfo| {
v.municipality()
.ok_or(ClineupError::MissingLocation("Municipality".to_string()))
.map(|v| v.to_string())
},
is_fallback
),
Placeholder::OriginalFilename => path.file_name().map_or_else(
|| {
is_fallback = true;
get_fallback_name("Original Filename")
},
|file_name| file_name.to_string_lossy().to_string(),
),
Placeholder::OriginalFolder => {
path.parent().and_then(|p| p.file_name()).map_or_else(
|| {
is_fallback = true;
get_fallback_name("Original Folder")
},
|dir_name| dir_name.to_string_lossy().to_string(),
)
}
Placeholder::Fallback => placeholder_text.clone(),
Placeholder::Unknown => placeholder_text.clone(),
};
result = current_result.clone();
if !is_fallback {
break;
}
}
formatted_path = formatted_path.replace(full_text, result.as_str());
}
Ok(Path::new(&formatted_path).to_path_buf())
}
}