Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 44 additions & 17 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)));
Expand All @@ -77,15 +90,38 @@ 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)
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...");

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();
Expand All @@ -101,23 +137,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)
Expand Down
2 changes: 1 addition & 1 deletion src/media_cleaning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
8 changes: 2 additions & 6 deletions src/metadata_embed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 11 additions & 8 deletions src/metadata_extraction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -20,7 +19,7 @@ pub struct MediaMetadata {
pub camera_model: Option<String>,
}

pub fn extract_metadata(base_path: &str) -> (Vec<MediaMetadata>, Vec<PathBuf>) {
pub fn extract_metadata(base_path: &str, guess_dates: bool) -> (Vec<MediaMetadata>, Vec<PathBuf>) {
let mut media_json_pairs: Vec<(PathBuf, PathBuf)> = Vec::new();
let mut all_media_files: Vec<PathBuf> = Vec::new();
let media_extensions = vec![
Expand Down Expand Up @@ -110,16 +109,20 @@ pub fn extract_metadata(base_path: &str) -> (Vec<MediaMetadata>, Vec<PathBuf>) {
}
// 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)
Expand Down