···11+use std::{
22+ fs,
33+ path::{Path, PathBuf},
44+ process::Command,
55+ sync::{
66+ Arc,
77+ atomic::{AtomicU64, Ordering},
88+ },
99+ time::Duration,
1010+};
1111+1212+use anyhow::bail;
1313+use exiftool::ExifTool;
1414+use infer::MatcherType;
1515+use log::{info, warn};
1616+use rayon::vec;
1717+use snowy_libs::multi_line::MultilineSession;
1818+1919+pub fn converty(silly: MultilineSession, path: PathBuf) -> anyhow::Result<()> {
2020+ if !path.is_dir() {
2121+ println!("{:?}", path.canonicalize());
2222+ bail!("Please provide an directory!")
2323+ }
2424+2525+ info!("Starting sillies");
2626+ // let files = fs::read_dir(path)?;
2727+2828+ info!("[D]:: {:?}", path);
2929+ // Image, Converted_dir, Fucked_convert_dir
3030+ let images = wurm(
3131+ &path,
3232+ Path::new("./converted/"),
3333+ Path::new("./fucked_convert/"),
3434+ )?;
3535+3636+ let count = Arc::new(AtomicU64::new(0));
3737+ let total_count: u64 = images.len() as u64;
3838+3939+ let bar = snowy_libs::start_progress_bar(
4040+ &count,
4141+ total_count,
4242+ 6,
4343+ "Converting media",
4444+ Duration::from_millis(100),
4545+ silly.clone(),
4646+ );
4747+4848+ let count = count.clone();
4949+ // let chunk_vec = chunk.to_vec();
5050+5151+ let mut exiftool = ExifTool::new().unwrap();
5252+5353+ images.iter().for_each(|(path, fixed_dir, fucked_dir)| {
5454+ match convert_image(path, fixed_dir, fucked_dir, &mut exiftool) {
5555+ Ok(_) => (),
5656+ Err(_err) => {
5757+ count.fetch_add(1, Ordering::Relaxed);
5858+ todo!();
5959+ }
6060+ }
6161+ count.fetch_add(1, Ordering::Relaxed);
6262+ });
6363+6464+ bar.join().unwrap();
6565+6666+ // threads.push(thread);
6767+6868+ // let chuncks = images.chunks(2000);
6969+7070+ // let mut threads = vec![];
7171+7272+ // for chunk in chuncks {
7373+ // let count = count.clone();
7474+ // let chunk_vec = chunk.to_vec();
7575+7676+ // let thread = thread::spawn(move || {
7777+ // let mut exiftool = ExifTool::new().unwrap();
7878+7979+ // chunk_vec
8080+ // .iter()
8181+ // .for_each(|(metadata, path, fixed_dir, fucked_dir)| {
8282+ // match handle_image(metadata, path, fixed_dir, fucked_dir, &mut exiftool) {
8383+ // Ok(_) => (),
8484+ // Err(_err) => {
8585+ // count.fetch_add(1, Ordering::Relaxed);
8686+ // todo!();
8787+ // }
8888+ // }
8989+ // count.fetch_add(1, Ordering::Relaxed);
9090+ // });
9191+ // });
9292+9393+ // threads.push(thread);
9494+ // }
9595+9696+ // for thread in threads {
9797+ // if thread.is_finished() {
9898+ // continue;
9999+ // }
100100+101101+ // thread.join().unwrap()
102102+ // }
103103+104104+ println!("Hello, world!");
105105+ Ok(())
106106+}
107107+108108+fn wurm(
109109+ dir: &Path,
110110+ converted_dir: &Path,
111111+ fucked_dir: &Path,
112112+) -> anyhow::Result<Vec<(PathBuf, PathBuf, PathBuf)>> {
113113+114114+ // Create the dirs
115115+ let _ = fs::create_dir(converted_dir);
116116+ let _ = fs::create_dir(fucked_dir);
117117+118118+ if !dir.is_dir() {
119119+ bail!("The dir isnt a directory?? \"{:?}\"", dir)
120120+ }
121121+122122+ let mut images = vec![];
123123+124124+ for entry in fs::read_dir(dir)? {
125125+ let entry = entry?;
126126+ let path = entry.path();
127127+128128+ if path.is_dir() {
129129+ let dir = path.file_name().unwrap();
130130+ let converted_dir = converted_dir.join(dir);
131131+ let fucked_dir = fucked_dir.join(dir);
132132+133133+ info!("[D]:: {path:?}");
134134+135135+ match wurm(&path, &converted_dir, &fucked_dir) {
136136+ Ok(val) => images.extend(val),
137137+ Err(err) => warn!("Error while worming into path \"{:?}\": {}", &path, err),
138138+ }
139139+ } else {
140140+ images.push((path, converted_dir.to_owned(), fucked_dir.to_owned()));
141141+ // info!("Converties");
142142+ }
143143+ }
144144+145145+ Ok(images)
146146+}
147147+148148+fn convert_image(
149149+ image: &Path,
150150+ converted_dir: &Path,
151151+ fucked_dir: &Path,
152152+ exif_tool: &mut ExifTool,
153153+) -> anyhow::Result<()> {
154154+ let kind = infer::get_from_path(image)?.expect("file type is unknown");
155155+156156+ let mut converted_path = converted_dir.join(image.file_name().unwrap());
157157+ let mut fucked_path = fucked_dir.join(image.file_name().unwrap());
158158+159159+ let media_type = if kind.extension() == "gif" {
160160+ // return Ok(());
161161+ kind.matcher_type()
162162+ // MatcherType::Video
163163+ } else {
164164+ kind.matcher_type()
165165+ };
166166+167167+ match media_type {
168168+ MatcherType::Image => {
169169+ return Ok(());
170170+ converted_path.set_extension("jxl");
171171+172172+ if converted_path.exists() {
173173+ log::info!("Skipping image {converted_path:?}");
174174+ return Ok(());
175175+ }
176176+177177+ let mut command = &mut Command::new("cjxl");
178178+179179+ command = command.args([
180180+ image.to_str().unwrap(),
181181+ converted_path.to_str().unwrap(),
182182+ "-e",
183183+ "1", // Pretty high effort (max is 11)
184184+ "-d",
185185+ "0", // Lossless
186186+ "--allow_expert_options",
187187+ "--container", // Encode it with an container format for exif
188188+ "1", // confirm
189189+ // "--lossless_jpeg", // Make it so jpeg can have a little bit of loss
190190+ // "1",
191191+ // "--num_threads",
192192+ // "24",
193193+ ]);
194194+195195+ // if kind.extension() != "gif" {
196196+ // command = command.arg("--progressive"); // Enable progresive decoding
197197+ // }
198198+199199+ let output = command.output().expect("Failed to convert image to jxl!");
200200+201201+ // .output()
202202+ // .expect("failed to execute process");
203203+204204+ if !output.status.success() {
205205+ log::error!("Error while converting {image:?} to jxl: {output:?}");
206206+207207+ if !fucked_dir.exists() {
208208+ fs::create_dir_all(fucked_dir).unwrap();
209209+ }
210210+211211+ fs::copy(image, fucked_path).unwrap();
212212+ return Ok(());
213213+ // std::process::exit(0);
214214+ } else {
215215+ log::info!(
216216+ "Converted {image:?} successfully. {} => {}",
217217+ fs::metadata(image)?.len(),
218218+ fs::metadata(&converted_path)?.len()
219219+ )
220220+ }
221221+ }
222222+ MatcherType::Video => {
223223+ converted_path.set_extension("mp4");
224224+ // Exiftool doesnt support matroska (mkv), so mp4 it is
225225+226226+ if converted_path.exists() {
227227+ log::info!("Skipping video {converted_path:?}");
228228+ return Ok(());
229229+ }
230230+231231+ let out = Command::new("ffmpeg")
232232+ .args([
233233+ "-y",
234234+ "-i",
235235+ image.to_str().unwrap(),
236236+ "-c:v",
237237+ "librav1e",
238238+ "-qp",
239239+ "40",
240240+ "-speed",
241241+ "2",
242242+ "/tmp/ffmpeg-converting.mp4",
243243+ ])
244244+ .output()
245245+ .expect("failed to execute process");
246246+247247+ if !out.status.success() {
248248+ log::error!("{out:?}");
249249+ std::process::exit(0);
250250+ } else {
251251+ // Make it so it puts files temp in /tmp
252252+ let out = Command::new("mv")
253253+ .args([
254254+ "/tmp/ffmpeg-converting.mp4",
255255+ converted_path.to_str().unwrap(),
256256+ ])
257257+ .output()
258258+ .expect("failed to move file");
259259+260260+ if !out.status.success() {
261261+ log::error!("{out:?}");
262262+ std::process::exit(0);
263263+ }
264264+265265+ let og_size = fs::metadata(image)?.len();
266266+ let conv_size = fs::metadata(&converted_path)?.len();
267267+268268+ log::info!(
269269+ "Converted {image:?} successfully. -{} ~{}%",
270270+ (og_size as i64 - conv_size as i64) as i64,
271271+ (conv_size / og_size * 100)
272272+ )
273273+ }
274274+ }
275275+ _ => {
276276+ log::warn!("This is not an image or video file, huh? {image:?}");
277277+ }
278278+ }
279279+280280+ // Copy over da exif
281281+ let args = &[
282282+ // "-T",
283283+ "-overwrite_original", // Don't make a copy with _original appended to it
284284+ "-TagsFromFile",
285285+ image.to_str().unwrap(),
286286+ "-all:all",
287287+ "-FileModifyDate<FileModifyDate", // Also copy the File Modification Date/Time
288288+ converted_path.to_str().unwrap(),
289289+ ];
290290+291291+ let out = exif_tool.execute_lines(args);
292292+293293+ // log::info!("{args:?} {out:?}");
294294+295295+ // Make sure exiftool did its thing correctly
296296+ if let Err(err) = out {
297297+ println!("{}", err.to_string().trim().replace("\n", ""));
298298+299299+ if !fucked_dir.exists() {
300300+ fs::create_dir_all(fucked_dir).unwrap();
301301+ }
302302+303303+ fucked_path.set_extension(converted_path.extension().unwrap());
304304+ fs::rename(&converted_path, &fucked_path).unwrap();
305305+ } else {
306306+ // log::error!("{:?}", out);
307307+ }
308308+309309+ // std::process::exit(0);
310310+311311+ Ok(())
312312+313313+ // Copy back exif metadata
314314+}
+165
gfoto-fixer/src/image_metadata.rs
···11+use std::{
22+ ffi::OsString,
33+ fs,
44+ path::{Path, PathBuf},
55+};
66+77+use anyhow::bail;
88+use regex::Regex;
99+use serde::Deserialize;
1010+1111+/// Struct that contains the relevant metadata of the metadata json files from google takeout
1212+#[derive(Debug, Deserialize, Clone)]
1313+pub struct GfotoMetadata {
1414+ pub title: String,
1515+ // description: String,
1616+ // imageViews: String,
1717+ #[serde(rename = "creationTime")]
1818+ pub creation_time: TimeInfo,
1919+ #[serde(rename = "photoTakenTime")]
2020+ pub photo_taken_time: TimeInfo,
2121+ // #[serde(rename = "geoData")]
2222+ // pub geo_data: GeoData,
2323+ // geoDataExif: Option<GeoData>,
2424+}
2525+2626+/// Struct that co
2727+#[derive(Debug, Deserialize, Clone)]
2828+pub struct TimeInfo {
2929+ pub timestamp: String,
3030+ // #[allow(dead_code)]
3131+ // formatted: String,
3232+}
3333+3434+// #[derive(Debug, Deserialize, PartialEq, Clone)]
3535+// pub struct GeoData {
3636+// pub latitude: f64,
3737+// pub longitude: f64,
3838+// pub altitude: f64,
3939+// #[serde(rename = "latitudeSpan")]
4040+// pub latitude_span: f64,
4141+// #[serde(rename = "longitudeSpan")]
4242+// pub longitude_span: f64,
4343+// }
4444+4545+/// Regex to see if it has the (num) sillieness in the image name.
4646+///
4747+/// It has a group for the base path, the num and the file extension
4848+pub static IMAGE_NAME_REGEX: lazy_regex::Lazy<Regex> =
4949+ lazy_regex::lazy_regex!(r"^(.*)\((\d+)\)(\.[^.]+)$");
5050+5151+/// Function that attempts to generate a metadata filename based on googles weird format
5252+///
5353+/// Google takeout limits file names to 46 chars for some reason
5454+fn generate_metadata_filename(path: &Path) -> anyhow::Result<OsString> {
5555+ let mut file_name = path.file_name().unwrap().to_os_string();
5656+5757+ // Add this
5858+ file_name.push(".supplemental-metadata");
5959+6060+ // Google takeout limits file names to 46 chars for some reason
6161+ // Not fully sure if this works safely, but it works for me... Might not work with weird chars.
6262+ file_name = match file_name.into_string() {
6363+ Ok(mut string) => {
6464+ if string.chars().count() > 46 {
6565+ while string.chars().count() > 46 {
6666+ string.pop();
6767+ }
6868+ OsString::from(string)
6969+ } else {
7070+ OsString::from(string)
7171+ }
7272+ }
7373+ Err(err) => bail!(
7474+ "Couldnt turn file_name into string. ({:?}) : {:?}",
7575+ path,
7676+ err
7777+ ),
7878+ };
7979+8080+ // Add .json file extension
8181+ file_name.push(".json");
8282+8383+ Ok(file_name)
8484+}
8585+8686+/// This function attempts to find the json metadata for the image
8787+/// Returns the metadata
8888+pub fn get_image_metadata(path: &Path) -> anyhow::Result<GfotoMetadata> {
8989+ // Generate the metadata file name
9090+ let metadata_string = match generate_metadata_filename(path) {
9191+ Ok(val) => val,
9292+ Err(err) => bail!("{err}"),
9393+ };
9494+9595+ // Generate the full path of the metadata file
9696+ let metadata_path = match path.parent() {
9797+ Some(parent) => parent.join(&metadata_string),
9898+ None => bail!("Couldnt get the folder of the file. ({:?})", path),
9999+ };
100100+101101+ // Try to read the metadata file
102102+ let file_content = match fs::read_to_string(metadata_path.clone()) {
103103+ Ok(val) => val,
104104+ Err(_err) => {
105105+ // Google likes to be silly when you make a copy of a file (i think?)
106106+ // If the original image was called "IMG_20180214_171835513.jpg" than the metadata file will be "IMG_20180214_171835513.jpg.supplemental-metada.json"
107107+ // Makes sense right? So what if I told you that copies do the following:
108108+ // The copy "IMG_20180214_171835513(1).jpg" has the metadata file "IMG_20180214_171835513.jpg.supplemental-metada(1).json"
109109+ // WHY???
110110+111111+ let str = path.file_name().unwrap().to_str().unwrap();
112112+113113+ // If it captures anything with the IMAGE_NAME_REGEX const
114114+ if let Some(caps) = IMAGE_NAME_REGEX.captures(str) {
115115+ let base = caps.get(1).unwrap().as_str();
116116+ let num = caps.get(2).unwrap().as_str();
117117+ let ext = caps.get(3).unwrap().as_str();
118118+119119+ let base_image: PathBuf = path.parent().unwrap().join(format!("{}{}", base, ext));
120120+121121+ let metadata_string = match generate_metadata_filename(&base_image) {
122122+ Ok(val) => val,
123123+ Err(err) => bail!("{err}"),
124124+ };
125125+126126+ let silly = metadata_string
127127+ .into_string()
128128+ .unwrap()
129129+ .replace(".json", &format!("({}).json", num));
130130+131131+ let new_path = path.parent().unwrap().join(silly);
132132+133133+ match fs::read_to_string(&new_path) {
134134+ Ok(val) => val,
135135+ Err(_err) => {
136136+ bail!(
137137+ "(2) Couldnt read the contents of the metadata file ({:?})",
138138+ &new_path,
139139+ // err
140140+ );
141141+ }
142142+ }
143143+ } else {
144144+ bail!(
145145+ "Couldnt read the contents of the metadata file ({:?})",
146146+ &metadata_path,
147147+ // err
148148+ );
149149+ }
150150+ }
151151+ };
152152+153153+ let metadata: GfotoMetadata = match serde_json::from_str(&file_content) {
154154+ Ok(val) => val,
155155+ Err(err) => {
156156+ bail!(
157157+ "Couldnt parse the metadata of the file ({:?}) : [{:?}]",
158158+ &metadata_path,
159159+ err
160160+ );
161161+ }
162162+ };
163163+164164+ Ok(metadata)
165165+}
+281
gfoto-fixer/src/main.rs
···11+use std::{
22+ fs,
33+ path::{Path, PathBuf},
44+ sync::{
55+ Arc,
66+ atomic::{AtomicU64, Ordering},
77+ },
88+ thread,
99+ time::Duration,
1010+ vec,
1111+};
1212+1313+use anyhow::{Context, bail};
1414+use clap::Parser;
1515+use env_logger::Env;
1616+use exiftool::ExifTool;
1717+use log::{info, warn};
1818+1919+use crate::image_metadata::GfotoMetadata;
2020+2121+mod convert;
2222+mod image_metadata;
2323+2424+const LOGGING_LEVEL: &str = "info";
2525+2626+/// Google takeout metadata fixer
2727+#[derive(Parser, Debug)]
2828+#[command(version, about, long_about = None)]
2929+struct Args {
3030+ /// Path to the images
3131+ #[arg(
3232+ short,
3333+ long,
3434+ default_value = "/mnt/SnowData/snowy/__STAGING__/Takeout/Test"
3535+ )]
3636+ images_folder: PathBuf,
3737+3838+ // // Overwrite existing time metadata
3939+ // #[arg(short, long, default_value_t = false)]
4040+ // overwrite_time: bool,
4141+4242+ // Only convert images in the fixed directory (handy incase you want to convert them later)
4343+ #[arg(short, long, default_value_t = false)]
4444+ only_conversion: bool,
4545+}
4646+4747+// IMG_20180929_172248549.jpg.supplemental-metada.json
4848+fn main() -> anyhow::Result<()> {
4949+ let mut silly = snowy_libs::multi_line::start_multiline_session();
5050+ let temp_silly = silly.clone();
5151+5252+ env_logger::Builder::from_env(Env::default().default_filter_or(LOGGING_LEVEL))
5353+ .format(move |buf, record| {
5454+ let msg = format!("[{}] - {}\n", record.level(), record.args());
5555+ // log_pb.println(msg).unwrap(); // Print above the progress bar
5656+ temp_silly.println(msg);
5757+ // println!();
5858+ Ok(())
5959+ })
6060+ .init();
6161+6262+ // env_logger::Builder::from_default_env().format(format)
6363+6464+ let args = Args::parse();
6565+ let path = args.images_folder;
6666+6767+ silly.reserve_line();
6868+ silly.reserve_line();
6969+ silly.reserve_line();
7070+ silly.reserve_line();
7171+7272+ if args.only_conversion {
7373+ convert::converty(silly.clone(), Path::new("./fixed/").to_path_buf())?;
7474+ return Ok(());
7575+ }
7676+7777+ if !path.is_dir() {
7878+ println!("{:?}", path.canonicalize());
7979+ bail!("Please provide an directory!")
8080+ }
8181+ info!("Starting sillies");
8282+ // let files = fs::read_dir(path)?;
8383+8484+ info!("[D]:: {:?}", path);
8585+ let images: Vec<(GfotoMetadata, PathBuf, PathBuf, PathBuf)> =
8686+ wurm(&path, Path::new("./fixed/"), Path::new("./fucked/"))?;
8787+8888+ let count = Arc::new(AtomicU64::new(0));
8989+ let total_count: u64 = images.len() as u64;
9090+9191+ let bar = snowy_libs::start_progress_bar(
9292+ &count,
9393+ total_count,
9494+ 6,
9595+ "Writing images",
9696+ Duration::from_millis(100),
9797+ silly.clone(),
9898+ );
9999+100100+ let chuncks = images.chunks(2000);
101101+102102+ let mut threads = vec![];
103103+104104+ for chunk in chuncks {
105105+ let count = count.clone();
106106+ let chunk_vec = chunk.to_vec();
107107+108108+ let thread = thread::spawn(move || {
109109+ let mut exiftool = ExifTool::new().unwrap();
110110+111111+ chunk_vec
112112+ .iter()
113113+ .for_each(|(metadata, path, fixed_dir, fucked_dir)| {
114114+ match handle_image(metadata, path, fixed_dir, fucked_dir, &mut exiftool) {
115115+ Ok(_) => (),
116116+ Err(_err) => {
117117+ count.fetch_add(1, Ordering::Relaxed);
118118+ todo!();
119119+ }
120120+ }
121121+ count.fetch_add(1, Ordering::Relaxed);
122122+ });
123123+ });
124124+125125+ threads.push(thread);
126126+ }
127127+128128+ for thread in threads {
129129+ if thread.is_finished() {
130130+ continue;
131131+ }
132132+133133+ thread.join().unwrap()
134134+ }
135135+136136+ bar.join().unwrap();
137137+138138+ println!("Hello, world!");
139139+ Ok(())
140140+}
141141+142142+fn wurm(
143143+ dir: &Path,
144144+ fixed_dir: &Path,
145145+ fucked_dir: &Path,
146146+) -> anyhow::Result<Vec<(GfotoMetadata, PathBuf, PathBuf, PathBuf)>> {
147147+ // println!("{:?}", putto_dir);
148148+149149+ // Create the dirs
150150+ fs::create_dir(fixed_dir).unwrap();
151151+ // fs::create_dir(fucked_dir).unwrap();
152152+153153+ if !dir.is_dir() {
154154+ bail!("The dir isnt a directory?? \"{:?}\"", dir)
155155+ }
156156+157157+ let mut images: Vec<(GfotoMetadata, PathBuf, PathBuf, PathBuf)> = vec![];
158158+159159+ for entry in fs::read_dir(dir)? {
160160+ let entry = entry?;
161161+ let path = entry.path();
162162+163163+ if path.is_dir() {
164164+ let dir = path.file_name().unwrap();
165165+ let fixed_dir = fixed_dir.join(dir);
166166+ let fucked_dir = fucked_dir.join(dir);
167167+168168+ info!("[D]:: {:?}", path);
169169+170170+ match wurm(&path, &fixed_dir, &fucked_dir) {
171171+ Ok(val) => images.extend(val),
172172+ Err(err) => warn!("Error while worming into path \"{:?}\": {}", &path, err),
173173+ }
174174+ } else {
175175+ // info!("Fixing exif");
176176+177177+ if path
178178+ .extension()
179179+ .context("Couldn't get the extensions of the path")?
180180+ == "json"
181181+ {
182182+ // info!("Skipping json file! {:?}", path);
183183+ continue;
184184+ }
185185+186186+ match image_metadata::get_image_metadata(&path) {
187187+ Ok(val) => {
188188+ // Image is good, push it!
189189+ images.push((val, path, fixed_dir.to_owned(), fucked_dir.to_owned()));
190190+ }
191191+ Err(err) => {
192192+ warn!("{} Skipping!", err);
193193+194194+ if !fucked_dir.exists() {
195195+ fs::create_dir_all(fucked_dir)?;
196196+ }
197197+198198+ // Copy the image to the fucked directory so people can sort them out themselves
199199+ fs::copy(
200200+ &path,
201201+ fucked_dir.join(
202202+ path.file_name()
203203+ .context("Couldn't get the file_name of the path")?,
204204+ ),
205205+ )?;
206206+207207+ continue;
208208+ }
209209+ };
210210+ }
211211+ }
212212+213213+ Ok(images)
214214+}
215215+216216+fn handle_image(
217217+ metadata: &GfotoMetadata,
218218+ path: &Path,
219219+ fixed_dir: &Path,
220220+ fucked_dir: &Path,
221221+ exiftool: &mut ExifTool,
222222+) -> anyhow::Result<()> {
223223+ // Extract needed data from the metadata
224224+ // let geodata = &metadata.geo_data;
225225+ let date_time_original_unix = &metadata.photo_taken_time.timestamp;
226226+ let create_date_unix = &metadata.creation_time.timestamp;
227227+ let name = &metadata.title;
228228+229229+ // Create paths for image
230230+ let mut fixed_path = fixed_dir.join(name);
231231+ let mut fucked_path = fucked_dir.join(name);
232232+233233+ // Read the raw image
234234+ let image = fs::read(path)?;
235235+236236+ {
237237+ // Fixup file extensions of images being incorrect
238238+ let kind = infer::get(&image).expect("file type is unknown");
239239+ fixed_path.set_extension(kind.extension());
240240+ fucked_path.set_extension(kind.extension());
241241+ }
242242+243243+ // Write the raw image back
244244+ fs::write(&fixed_path, image)?;
245245+246246+ // Arguments for exiftool
247247+ let args = &[
248248+ // "-T",
249249+ "-m",
250250+ "-d",
251251+ "%s",
252252+ // The timestamp tags
253253+ "-overwrite_original",
254254+ &format!("-DateTimeOriginal={}", date_time_original_unix),
255255+ &format!("-CreateDate={}", create_date_unix),
256256+ &format!("-ModifyDate={}", create_date_unix),
257257+ &format!("-FileModifyDate={}", create_date_unix),
258258+ // &format!("-GPSLatitude={}", geodata.latitude),
259259+ // &format!("-GPSLongitude={}", geodata.longitude),
260260+ // &format!("-GPSAltitude={}", geodata.altitude),
261261+ // File path :3
262262+ fixed_path.to_str().unwrap(),
263263+ ];
264264+265265+ let out = exiftool.execute_lines(args);
266266+267267+ // Todo! gps data
268268+269269+ // Make sure exiftool did its thing correctly
270270+ if let Err(err) = out {
271271+ log::error!("{}", err.to_string().trim().replace("\n", ""));
272272+273273+ if !fucked_dir.exists() {
274274+ fs::create_dir_all(fucked_dir).unwrap();
275275+ }
276276+277277+ fs::rename(&fixed_path, &fucked_path).unwrap();
278278+ }
279279+280280+ Ok(())
281281+}
+15
justfile
···11+22+server:
33+ cargo run --bin converter-server
44+55+server-release:
66+ cargo run --bin converter-server --release
77+88+client:
99+ cargo run --bin converter-client
1010+1111+client-release:
1212+ cargo run --bin converter-client
1313+1414+client-windows:
1515+ nix develop .#windows --command cargo build --bin converter-client --release --target x86_64-pc-windows-gnu
+23
readme.md
···11+# gfoto-metadata-fixer
22+## The script
33+I havent been able to find a script that handles the image restoration correctly, so i decided to write my own.
44+This cli app is very fast, however it still depends on how fast your drive is!
55+66+This script does the following:
77+- Restores original file names
88+- Fixes incorrect file extensions (needed for fixing exif data)
99+- Restores EXIF OriginallyCreated data
1010+- It copies the files and the into a new directory (with the same structure) instead of overwriting the existing files.
1111+- If it cant find the metadata for a file it will put it in a *seperate directory*
1212+1313+It handles some bullshit that google does that I have encountered myself:
1414+- Google trims the metadata json to 46 chars
1515+- Duplicate files that google decides to give a weird metadata file.
1616+ - "IMG_20180214_171835513(1).jpg" has the metadata file "IMG_20180214_171835513.jpg.supplemental-metada(1).json"
1717+1818+1919+## The converter
2020+This repo also includes a jxl and av1 conversion project. It takes all the images and convertes them to jxl/av1 spread out over seperate computers.
2121+2222+There is a server component that distrebutes the files, and a client component that converts it to jxl/av1, restores the exif, and sents it back to the server.
2323+The server than stores it in two folders, converted or converted_fucked, depending if the conversion succeeded or not :P.
+102
test_script.sh
···11+#! /usr/bin/env nix-shell
22+#! nix-shell -i bash -p coreutils bash exiftool jq
33+# set -x
44+base_dir=$1
55+#echo $image_dir
66+77+# Goes trought the current directory, lists everything and goes through every file
88+# It will go
99+wurm() {
1010+ local image_dir=$1
1111+1212+ for file in "$image_dir"/*; do
1313+1414+ if [[ -d "$file" ]]; then
1515+1616+ # Found a new directory, worming in!
1717+ echo "[D]:: $file"
1818+ wurm "$file"
1919+ elif [[ -f "$file" ]]; then
2020+ # echo "[F]:: $file"
2121+ process_file "$file"
2222+ # echo -n
2323+ else
2424+ echo "WHAT THE SIGMA $file"
2525+ fi
2626+2727+ done
2828+}
2929+3030+3131+process_file() {
3232+ if [[ "$1" == *.json ]]; then
3333+ return 1
3434+ fi
3535+3636+3737+3838+ # Get the file name of the file
3939+ # ./F/IMG_20170703_210233231.jpg
4040+ # file="$1"
4141+ file_name=$(basename "$1")
4242+ file_dir=$(dirname "$1")
4343+ # echo $file_dir
4444+4545+4646+ # now to_save_dir=$() == file_dir - base_dir from beginnign
4747+4848+4949+ # to_save_dir=$(realpath --relative-to="$base_dir" "$file_dir")
5050+ # echo $to_save_dir_fixed
5151+5252+ # exit 0
5353+5454+ json_file="${file_name}.supplemental-metadata"
5555+5656+ json_file=${json_file:0:46} # Truncate to 46 chars if needed
5757+5858+ json_file="${file_dir}/${json_file}.json"
5959+6060+ if [[ -f $json_file ]]; then
6161+ # f
6262+ # echo -n
6363+ temp_file=$(cat "$json_file")
6464+ # IMG_20180822_182349199_BURST011-EFFECTS.jpg.su.json
6565+ # IMG_20180822_182349199_BURST011-EFFECTS-bewerkt.jpg
6666+ new_name=$(echo -n $temp_file | jq .title)
6767+ geo_lati=$(echo -n $temp_file | jq .geoData.latitude)
6868+ geo_long=$(echo -n $temp_file | jq .geoData.longitude)
6969+ geo_alti=$(echo -n $temp_file | jq .geoData.altitude)
7070+7171+ geo_alti_span=$(echo -n $temp_file | jq .geoData.latitudeSpan)
7272+ geo_long_span=$(echo -n $temp_file | jq .geoData.longitudeSpan)
7373+7474+ date_time_original_unix=$(echo -n $temp_file | jq .photoTakenTime.timestamp)
7575+7676+ # to_save_dir="./fixed/${file_dir#"$base_dir"/}"
7777+ # mkdir -p "$to_save_dir"
7878+ # cp "$1" "$to_save_dir/$new_name"
7979+ # exit 1
8080+8181+ else
8282+ echo "[E]:: Json metadata not found for: \"$1\""
8383+ # to_save_dir="./fucked/${file_dir#"$base_dir"/}"
8484+ # mkdir -p "$to_save_dir"
8585+ # cp "$1" "$to_save_dir/$file_name"
8686+ # exit 1
8787+ fi
8888+8989+9090+9191+ # new_path
9292+9393+ # echo "$1 :: ${file_name} :: ${json_file}"
9494+ # exit 0
9595+}
9696+9797+# Make dirs
9898+mkdir ./fixed
9999+mkdir ./fucked
100100+101101+# echo $base_dir
102102+wurm "$base_dir"