From 89e9ef7363a803b6652773de62e7846746be9d5a Mon Sep 17 00:00:00 2001 From: Donncha O Caoimh <5656673+donnchawp@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:37:36 +0000 Subject: [PATCH 1/2] Move all user prompts to start of process Collect all interactive input (WhatsApp/Screenshot separation and metadata embedding source) upfront before file copying and processing begins, so the tool can run unattended after initial setup. Also rename ask_and_separate_whatsapp_screenshots to separate_whatsapp_screenshots since it no longer prompts the user. --- src/main.rs | 40 +++++++++++++++++++++++++------------- src/media_cleaning.rs | 2 +- src/metadata_embed.rs | 8 ++------ src/metadata_extraction.rs | 19 ++++++++++-------- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6c806c5..00fddf1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,6 +80,29 @@ fn main() { let output_dir = PathBuf::from(output.trim()); let temp_dir = output_dir.join("MetaSort_temp"); + // Ask if WhatsApp/Screenshots should be separated (before processing begins) + println!("\nDo you want to separate WhatsApp and Screenshot images? (y/n)"); + let mut wa_sc_input = String::new(); + io::stdin().read_line(&mut wa_sc_input).expect("Failed to read line"); + let separate_wa_sc = matches!(wa_sc_input.trim().to_lowercase().as_str(), "y" | "yes"); + if separate_wa_sc { + MetaSortUI::print_success("WhatsApp and Screenshot images will be sorted into their own folders by year/month."); + } else { + MetaSortUI::print_info("WhatsApp and Screenshot images will be treated as regular photos."); + } + + // Ask how to embed date/time for WhatsApp & Screenshot images + println!("\nEmbed date/time for WhatsApp & Screenshot images based on their:\n1. Metadata\n2. Filename"); + let mut embed_input = String::new(); + io::stdin().read_line(&mut embed_input).expect("Failed to read line"); + let use_filename_date = matches!(embed_input.trim(), "2"); + + // Ask how to handle media files with no matching JSON + println!("\nIf media files have no matching .json, MetaSort should:\n1. Skip and move to 'Unknown Time'\n2. Try to guess timestamp from filename"); + let mut unpaired_input = String::new(); + io::stdin().read_line(&mut unpaired_input).expect("Failed to read line"); + let guess_dates_from_filename = matches!(unpaired_input.trim(), "2"); + // Copy input folder to MetaSort_temp in output directory MetaSortUI::print_section_header("Copying Files"); MetaSortUI::print_info("Copying input folder to working directory..."); @@ -101,23 +124,14 @@ fn main() { media_cleaning::clean_json_filenames(temp_dir.to_str().unwrap()); MetaSortUI::print_success("JSON filename cleaning and pairing complete!"); - // 1b. Ask if WhatsApp/Screenshots should be separated - println!("\nDo you want to separate WhatsApp and Screenshot images? (y/n)"); - let mut wa_sc_input = String::new(); - io::stdin().read_line(&mut wa_sc_input).expect("Failed to read line"); - let separate_wa_sc = matches!(wa_sc_input.trim().to_lowercase().as_str(), "y" | "yes"); - if separate_wa_sc { - MetaSortUI::print_success("WhatsApp and Screenshot images will be sorted into their own folders by year/month."); - } else { - MetaSortUI::print_info("WhatsApp and Screenshot images will be treated as regular photos."); - } - media_cleaning::ask_and_separate_whatsapp_screenshots(temp_dir.to_str().unwrap(), separate_wa_sc); + // 1b. Separate WhatsApp/Screenshots if requested + media_cleaning::separate_whatsapp_screenshots(temp_dir.to_str().unwrap(), separate_wa_sc); // 2. Extract metadata from JSON and embed into media files MetaSortUI::print_section_header("Metadata Extraction and Embedding"); MetaSortUI::print_info("Extracting metadata from JSON and embedding into media files..."); - let (metadata, failed_guess_paths) = metadata_extraction::extract_metadata(temp_dir.to_str().unwrap()); - metadata_embed::embed_metadata_all(&metadata, &temp_dir); + let (metadata, failed_guess_paths) = metadata_extraction::extract_metadata(temp_dir.to_str().unwrap(), guess_dates_from_filename); + metadata_embed::embed_metadata_all(&metadata, &temp_dir, use_filename_date); MetaSortUI::print_success("Metadata extraction and embedding complete!"); // 3. Sort files using the embedded metadata (DateTimeOriginal) diff --git a/src/media_cleaning.rs b/src/media_cleaning.rs index 597b54e..62d4e15 100644 --- a/src/media_cleaning.rs +++ b/src/media_cleaning.rs @@ -8,7 +8,7 @@ use regex::Regex; use std::io::{self, Write}; use crate::utils::log_to_file; -pub fn ask_and_separate_whatsapp_screenshots(base_path: &str, separate_wa_sc: bool) { +pub fn separate_whatsapp_screenshots(base_path: &str, separate_wa_sc: bool) { if !separate_wa_sc { return; } diff --git a/src/metadata_embed.rs b/src/metadata_embed.rs index e50bc64..c47e8e4 100644 --- a/src/metadata_embed.rs +++ b/src/metadata_embed.rs @@ -2,22 +2,18 @@ // Embedding metadata logic for MetaSort_v1.0.0 – Google Photos Takeout Organizer use std::fs::{self, File}; -use std::io::{self, Write}; +use std::io::Write; use std::path::Path; use crate::metadata_extraction::MediaMetadata; use crate::filename_date_guess::extract_date_from_filename; use crate::utils::log_to_file; use crate::platform::get_exiftool_command; -pub fn embed_metadata_all(metadata_list: &[MediaMetadata], log_dir: &Path) { +pub fn embed_metadata_all(metadata_list: &[MediaMetadata], log_dir: &Path, use_filename: bool) { let logs_dir = log_dir.join("logs"); let log_path = logs_dir.join("metadata_embedding.log"); let _ = fs::create_dir_all(&logs_dir); let _log_file = File::create(&log_path).expect("Failed to create log file"); - println!("\n🧐Do you want to embed date/time for WhatsApp & Screenshot images based on their \n1. Metadata\n2. Filename\n"); - let mut input = String::new(); - io::stdin().read_line(&mut input).expect("Failed to read line"); - let use_filename = matches!(input.trim(), "2"); let total = metadata_list.len(); let mut processed = 0; for meta in metadata_list { diff --git a/src/metadata_extraction.rs b/src/metadata_extraction.rs index a7434cd..6b1cadd 100644 --- a/src/metadata_extraction.rs +++ b/src/metadata_extraction.rs @@ -4,7 +4,6 @@ use walkdir::WalkDir; use serde_json::Value; use chrono::{TimeZone, Utc}; use crate::utils::log_to_file; -use std::io; use std::io::Write; use crate::filename_date_guess::extract_date_from_filename; @@ -20,7 +19,7 @@ pub struct MediaMetadata { pub camera_model: Option, } -pub fn extract_metadata(base_path: &str) -> (Vec, Vec) { +pub fn extract_metadata(base_path: &str, guess_dates: bool) -> (Vec, Vec) { let mut media_json_pairs: Vec<(PathBuf, PathBuf)> = Vec::new(); let mut all_media_files: Vec = Vec::new(); let media_extensions = vec![ @@ -110,16 +109,20 @@ pub fn extract_metadata(base_path: &str) -> (Vec, Vec) { } // Handle unpaired media if !unpaired_media.is_empty() { + let total_files = paired_media.len() + unpaired_media.len(); + let pct = (unpaired_media.len() * 100) / total_files; println!( - "\n⚠️ No .json found for {} out of {} files ({}%).\nWhat should MetaSort do?\n1. Skip and move to 'Unknown Time'\n2. Try to guess timestamp from filename\nEnter 1 or 2:", - unpaired_media.len(), paired_media.len() + unpaired_media.len(), (unpaired_media.len() * 100) / (paired_media.len() + unpaired_media.len()) + "\n⚠️ No .json found for {} out of {} files ({}%).", + unpaired_media.len(), total_files, pct ); - let mut input = String::new(); - io::stdin().read_line(&mut input).unwrap(); - let guess = input.trim() == "2"; + if guess_dates { + println!("Attempting to guess timestamps from filenames..."); + } else { + println!("Moving these files to 'Unknown Time'."); + } for media_path in unpaired_media { let filename = media_path.file_name().and_then(|n| n.to_str()).unwrap_or(""); - let exif_date = if guess { + let exif_date = if guess_dates { if let Some(date) = extract_date_from_filename(filename) { log_to_file(&logs_dir, "metadata_extraction.log", &format!("Guessed date from filename for {:?}: {}", filename, date)); Some(date) From 08577134cf1d30d1841e45f32c917f1fbf60e984 Mon Sep 17 00:00:00 2001 From: Donncha O Caoimh <5656673+donnchawp@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:49:19 +0000 Subject: [PATCH 2/2] Fix paths with backslash-escaped spaces from terminal drag-and-drop --- src/main.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 00fddf1..1ade68b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,19 @@ use fs_extra; use crate::platform::{is_exiftool_available, get_installation_instructions}; use crate::ui::MetaSortUI; +/// Clean a path string from terminal input. +/// Terminals may wrap dragged paths in quotes or escape spaces with backslashes. +/// On Windows, backslashes are path separators and should not be stripped. +fn clean_input_path(s: &str) -> String { + // Strip surrounding single or double quotes + let s = s.trim_matches(|c| c == '\'' || c == '"'); + if cfg!(windows) { + s.to_string() + } else { + s.replace("\\ ", " ") + } +} + fn get_folder_size(path: &str) -> u64 { walkdir::WalkDir::new(path) .into_iter() @@ -58,10 +71,10 @@ fn main() { println!("\n📂 Please drag and drop your Google Photos Takeout folder here, or specify the folder path:"); let mut input = String::new(); io::stdin().read_line(&mut input).expect("Failed to read line"); - let input_dir = input.trim(); + let input_dir = clean_input_path(input.trim()); // Calculate input folder size and prompt for required space - let folder_size = get_folder_size(input_dir); + let folder_size = get_folder_size(&input_dir); let required_space = folder_size * 3; MetaSortUI::print_info(&format!("Input folder size: {}", human_readable_size(folder_size))); MetaSortUI::print_info(&format!("Recommended free space: {}", human_readable_size(required_space))); @@ -77,7 +90,7 @@ fn main() { println!("\n📁 Please specify the output folder where MetaSort should work (originals will be untouched):"); let mut output = String::new(); io::stdin().read_line(&mut output).expect("Failed to read line"); - let output_dir = PathBuf::from(output.trim()); + let output_dir = PathBuf::from(clean_input_path(output.trim())); let temp_dir = output_dir.join("MetaSort_temp"); // Ask if WhatsApp/Screenshots should be separated (before processing begins) @@ -108,7 +121,7 @@ fn main() { MetaSortUI::print_info("Copying input folder to working directory..."); let mut ui = MetaSortUI::new(); - let total_files = count_files_in_directory(input_dir); + let total_files = count_files_in_directory(&input_dir); ui.start_main_progress(total_files as u64, "Copying files"); let mut copy_options = fs_extra::dir::CopyOptions::new();