├── assets ├── upi.png └── logo.png ├── src ├── .DS_Store ├── utils.rs ├── csv_report.rs ├── platform.rs ├── metadata_embed.rs ├── ui.rs ├── html_report.rs ├── media_cleaning.rs ├── metadata_extraction.rs ├── filename_date_guess.rs ├── main.rs └── sort_to_folders.rs ├── .github └── FUNDING.yml ├── Cargo.toml ├── scripts ├── build_windows.bat ├── build_all.sh ├── install_windows.bat ├── build_macos.sh └── install_windows.ps1 ├── PROJECT_STRUCTURE.md ├── docs ├── SIMPLE_INSTALL.md └── CROSS_PLATFORM_CHANGES.md ├── LICENSE.txt ├── README.md └── Cargo.lock /assets/upi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamsanmith/MetaSort/HEAD/assets/upi.png -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamsanmith/MetaSort/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamsanmith/MetaSort/HEAD/assets/logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: 5 | open_collective: 6 | ko_fi: iamsanmith 7 | tidelift: 8 | community_bridge: 9 | liberapay: 10 | issuehunt: 11 | lfx_crowdfunding: 12 | polar: 13 | buy_me_a_coffee: 14 | thanks_dev: 15 | custom: [upier.vercel.app/pay/sanmith@superyes] 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "MetaSort" 3 | version = "1.0.0" 4 | edition = "2021" 5 | authors = ["Sanmith S"] 6 | description = "MetaSort v1 – Cross-platform Google Photos Takeout Organizer" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/iamsanmith/MetaSort" 9 | 10 | [dependencies] 11 | walkdir = "2" 12 | regex = "1" 13 | serde = { version = "1", features = ["derive"] } 14 | serde_json = "1" 15 | chrono = "0.4" 16 | csv = "1.3" 17 | fs_extra = "1.3" 18 | url = "2" 19 | indicatif = "0.17" 20 | 21 | # Note: exiftool must be installed on the system (external dependency) 22 | # Cross-platform support: macOS, Windows, and Linux -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | // utils.rs 2 | // Utility/helper functions for MetaSort_v1.0.0 – Google Photos Takeout Organizer 3 | 4 | use std::fs::{OpenOptions, create_dir_all}; 5 | use std::io::Write; 6 | use std::path::Path; 7 | use chrono::Local; 8 | 9 | /// Appends a timestamped log entry to a log file in the logs folder inside the given directory. 10 | pub fn log_to_file(log_dir: &Path, log_name: &str, message: &str) { 11 | let _ = create_dir_all(log_dir); 12 | let log_path = log_dir.join(log_name); 13 | let mut file = OpenOptions::new() 14 | .create(true) 15 | .append(true) 16 | .open(&log_path) 17 | .expect("Unable to open log file"); 18 | let now = Local::now().format("[%Y-%m-%d %H:%M:%S]"); 19 | let _ = writeln!(file, "{} {}", now, message); 20 | } -------------------------------------------------------------------------------- /scripts/build_windows.bat: -------------------------------------------------------------------------------- 1 | cd /d "%~dp0\.." 2 | @echo off 3 | echo ======================================== 4 | echo Building MetaSort for Windows 5 | echo ======================================== 6 | echo. 7 | 8 | echo Building release executable... 9 | cargo build --release --target x86_64-pc-windows-msvc 10 | 11 | if %errorlevel% neq 0 ( 12 | echo ❌ Build failed! 13 | pause 14 | exit /b 1 15 | ) 16 | 17 | echo. 18 | echo ✅ Build successful! 19 | echo. 20 | echo Creating single-click executable... 21 | 22 | REM Create a simple launcher script 23 | echo @echo off > MetaSort.exe 24 | echo echo Starting MetaSort... >> MetaSort.exe 25 | echo echo. >> MetaSort.exe 26 | echo target\x86_64-pc-windows-msvc\release\MetaSort.exe >> MetaSort.exe 27 | echo pause >> MetaSort.exe 28 | 29 | echo. 30 | echo 🎉 MetaSort.exe created successfully! 31 | echo. 32 | echo Users can now: 33 | echo 1. Double-click MetaSort.exe to run 34 | echo 2. No installation required 35 | echo 3. Just make sure exiftool is installed 36 | echo. 37 | echo The executable is in: MetaSort.exe 38 | echo. 39 | pause 40 | -------------------------------------------------------------------------------- /src/csv_report.rs: -------------------------------------------------------------------------------- 1 | // csv_report.rs 2 | // CSV report generation logic for MetaSort_v1.0.0 – Google Photos Takeout Organizer 3 | 4 | use std::path::Path; 5 | use csv; 6 | 7 | /// Write a CSV report for a given folder and set of files. 8 | pub fn write_csv_report( 9 | folder: &Path, 10 | files: &[(String, String, String, String, String, u64)], // (FileName, Filetype, Original Time, Resolution, Human Size, Size) 11 | csv_name: &str, 12 | ) { 13 | let csv_path = folder.join(csv_name); 14 | let mut wtr = csv::Writer::from_path(&csv_path).expect("Failed to create CSV file"); 15 | wtr.write_record(&["SL", "FileName", "Filetype", "Original Time", "File Resolution", "File Size", "Bytes"]).unwrap(); 16 | for (i, (filename, filetype, orig_time, resolution, human_size, size)) in files.iter().enumerate() { 17 | wtr.write_record(&[ 18 | (i + 1).to_string(), 19 | filename.to_string(), 20 | filetype.to_string(), 21 | orig_time.to_string(), 22 | resolution.to_string(), 23 | human_size.to_string(), 24 | size.to_string(), 25 | ]).unwrap(); 26 | } 27 | wtr.flush().unwrap(); 28 | } -------------------------------------------------------------------------------- /scripts/build_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "========================================" 4 | echo "Building MetaSort for All Platforms" 5 | echo "========================================" 6 | echo 7 | 8 | # Detect platform 9 | if [[ "$OSTYPE" == "darwin"* ]]; then 10 | PLATFORM="macos" 11 | elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then 12 | PLATFORM="windows" 13 | else 14 | PLATFORM="linux" 15 | fi 16 | 17 | echo "Detected platform: $PLATFORM" 18 | echo 19 | 20 | # Build for current platform 21 | echo "Building for $PLATFORM..." 22 | if [[ "$PLATFORM" == "macos" ]]; then 23 | ./build_macos.sh 24 | elif [[ "$PLATFORM" == "windows" ]]; then 25 | ./build_windows.bat 26 | else 27 | echo "Building Linux executable..." 28 | cargo build --release 29 | echo "✅ Linux executable created: target/release/MetaSort" 30 | fi 31 | 32 | echo 33 | echo "========================================" 34 | echo "Build Complete!" 35 | echo "========================================" 36 | echo 37 | echo "For distribution:" 38 | echo "1. Copy the executable to users" 39 | echo "2. Ensure exiftool is installed on their system" 40 | echo "3. Users can double-click to run" 41 | echo -------------------------------------------------------------------------------- /scripts/install_windows.bat: -------------------------------------------------------------------------------- 1 | cd /d "%~dp0\.." 2 | @echo off 3 | echo ======================================== 4 | echo MetaSort Windows Installation Helper 5 | echo ======================================== 6 | echo. 7 | 8 | echo Checking if Rust is installed... 9 | rustc --version >nul 2>&1 10 | if %errorlevel% neq 0 ( 11 | echo ❌ Rust is not installed. 12 | echo. 13 | echo Please install Rust from: https://rustup.rs/ 14 | echo After installation, restart this script. 15 | pause 16 | exit /b 1 17 | ) else ( 18 | echo ✅ Rust is installed. 19 | ) 20 | 21 | echo. 22 | echo Checking if exiftool is installed... 23 | exiftool -ver >nul 2>&1 24 | if %errorlevel% neq 0 ( 25 | echo ❌ ExifTool is not installed. 26 | echo. 27 | echo Installing ExifTool using winget... 28 | winget install -e --id OliverBetz.ExifTool 29 | if %errorlevel% neq 0 ( 30 | echo. 31 | echo Winget installation failed. Trying Chocolatey... 32 | choco install exiftool 33 | if %errorlevel% neq 0 ( 34 | echo. 35 | echo ❌ Automatic installation failed. 36 | echo. 37 | echo Please install ExifTool manually: 38 | echo 1. Download from https://exiftool.org/ 39 | echo 2. Extract to C:\exiftool 40 | echo 3. Add C:\exiftool to your PATH environment variable 41 | echo 4. Restart your command prompt 42 | pause 43 | exit /b 1 44 | ) 45 | ) 46 | echo. 47 | echo ✅ ExifTool installed successfully! 48 | echo Please restart your command prompt for PATH changes to take effect. 49 | ) else ( 50 | echo ✅ ExifTool is installed. 51 | ) 52 | 53 | echo. 54 | echo Building MetaSort... 55 | cargo build --release 56 | if %errorlevel% neq 0 ( 57 | echo ❌ Build failed. Please check the error messages above. 58 | pause 59 | exit /b 1 60 | ) 61 | 62 | echo. 63 | echo ✅ MetaSort built successfully! 64 | echo. 65 | echo You can now run MetaSort with: 66 | echo cargo run --release 67 | echo. 68 | pause 69 | -------------------------------------------------------------------------------- /src/platform.rs: -------------------------------------------------------------------------------- 1 | // platform.rs 2 | // Cross-platform utilities for MetaSort 3 | 4 | use std::process::Command; 5 | use std::env; 6 | 7 | #[cfg(target_os = "windows")] 8 | pub const EXIFTOOL_CMD: &str = "exiftool.exe"; 9 | 10 | #[cfg(not(target_os = "windows"))] 11 | pub const EXIFTOOL_CMD: &str = "exiftool"; 12 | 13 | /// Check if exiftool is available on the system 14 | pub fn is_exiftool_available() -> bool { 15 | let output = Command::new(EXIFTOOL_CMD) 16 | .arg("-ver") 17 | .output(); 18 | 19 | match output { 20 | Ok(output) => output.status.success(), 21 | Err(_) => false, 22 | } 23 | } 24 | 25 | /// Get the exiftool command with proper path handling 26 | pub fn get_exiftool_command() -> Command { 27 | Command::new(EXIFTOOL_CMD) 28 | } 29 | 30 | /// Get platform-specific installation instructions 31 | pub fn get_installation_instructions() -> String { 32 | match env::consts::OS { 33 | "windows" => { 34 | r#" 35 | 📋 Windows Installation Instructions: 36 | 37 | 1. Install Rust: https://rustup.rs/ 38 | 2. Install exiftool using one of these methods: 39 | 40 | Option A (Recommended): Using Chocolatey 41 | ```cmd 42 | choco install exiftool 43 | ``` 44 | 45 | Option B: Using winget 46 | ```cmd 47 | winget install ExifTool.ExifTool 48 | ``` 49 | 50 | Option C: Manual installation 51 | - Download from https://exiftool.org/ 52 | - Extract to C:\exiftool 53 | - Add C:\exiftool to your PATH environment variable 54 | 55 | 3. Restart your command prompt after installation 56 | "#.to_string() 57 | }, 58 | "macos" => { 59 | r#" 60 | 📋 macOS Installation Instructions: 61 | 62 | 1. Install Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 63 | 2. Install exiftool: 64 | ```sh 65 | brew install exiftool 66 | ``` 67 | "#.to_string() 68 | }, 69 | _ => { 70 | r#" 71 | 📋 Linux Installation Instructions: 72 | 73 | 1. Install Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 74 | 2. Install exiftool: 75 | ```sh 76 | # Ubuntu/Debian 77 | sudo apt-get install exiftool 78 | 79 | # CentOS/RHEL/Fedora 80 | sudo yum install perl-Image-ExifTool 81 | # or 82 | sudo dnf install perl-Image-ExifTool 83 | ``` 84 | "#.to_string() 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /scripts/build_macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "========================================" 4 | echo "Building MetaSort for macOS" 5 | echo "========================================" 6 | echo 7 | 8 | echo "Building release executable..." 9 | cargo build --release 10 | 11 | if [ $? -ne 0 ]; then 12 | echo "❌ Build failed!" 13 | exit 1 14 | fi 15 | 16 | echo 17 | echo "✅ Build successful!" 18 | echo 19 | echo "Creating macOS launchers..." 20 | 21 | # Create a simple command file that runs directly 22 | cat > MetaSort.command << 'EOF' 23 | #!/bin/bash 24 | 25 | # Change to the directory where this script is located 26 | cd "$(dirname "$0")" 27 | 28 | # Check if exiftool is installed 29 | if ! command -v exiftool &> /dev/null; then 30 | echo "❌ ExifTool is not installed!" 31 | echo "Please install it first:" 32 | echo "brew install exiftool" 33 | echo "" 34 | echo "Press Enter to exit..." 35 | read 36 | exit 1 37 | fi 38 | 39 | # Run MetaSort 40 | echo "🚀 Starting MetaSort..." 41 | echo "" 42 | 43 | # Run the executable directly 44 | ./target/release/MetaSort 45 | 46 | # Keep terminal open if there's an error 47 | if [ $? -ne 0 ]; then 48 | echo "" 49 | echo "Press Enter to exit..." 50 | read 51 | fi 52 | EOF 53 | 54 | # Create a launcher that opens in new terminal window 55 | cat > Run_MetaSort.command << 'EOF' 56 | #!/bin/bash 57 | 58 | # Get the directory where this script is located 59 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 60 | 61 | # Open a new terminal window and run MetaSort 62 | osascript << EOSCRIPT 63 | tell application "Terminal" 64 | do script "cd '$SCRIPT_DIR' && ./target/release/MetaSort" 65 | activate 66 | end tell 67 | EOSCRIPT 68 | EOF 69 | 70 | # Make both executable 71 | chmod +x MetaSort.command 72 | chmod +x Run_MetaSort.command 73 | 74 | echo 75 | echo "🎉 MetaSort launchers created successfully!" 76 | echo 77 | echo "For non-technical users:" 78 | echo "1. Double-click 'Run_MetaSort.command' (opens in new terminal)" 79 | echo "2. Or double-click 'MetaSort.command' (runs in current terminal)" 80 | echo 81 | echo "Both options will:" 82 | echo "✅ Check if ExifTool is installed" 83 | echo "✅ Start MetaSort automatically" 84 | echo "✅ Show clear error messages if something is wrong" 85 | echo "✅ Keep the window open if there are errors" 86 | echo 87 | echo "The launchers are:" 88 | echo "- Run_MetaSort.command (recommended for non-technical users)" 89 | echo "- MetaSort.command (for advanced users)" 90 | echo -------------------------------------------------------------------------------- /PROJECT_STRUCTURE.md: -------------------------------------------------------------------------------- 1 | # 📁 MetaSort Project Structure 2 | 3 | ## 🎯 Quick Start Files (Root Directory) 4 | - **`Run_MetaSort.command`** - Double-click to open MetaSort in new terminal (recommended for non-technical users) 5 | - **`MetaSort.command`** - Double-click to run MetaSort in current terminal 6 | - **`README.md`** - Main documentation and installation guide 7 | 8 | ## 📂 Organized Directories 9 | 10 | ### `/scripts/` - Build and Installation Scripts 11 | - **`build_macos.sh`** - Builds MetaSort and creates macOS launchers 12 | - **`build_windows.bat`** - Builds MetaSort for Windows 13 | - **`build_all.sh`** - Universal build script (detects platform) 14 | - **`install_windows.ps1`** - PowerShell installation helper for Windows 15 | - **`install_windows.bat`** - Batch installation helper for Windows 16 | 17 | ### `/docs/` - Documentation 18 | - **`SIMPLE_INSTALL.md`** - Step-by-step guide for non-technical users 19 | - **`CROSS_PLATFORM_CHANGES.md`** - Technical details of cross-platform implementation 20 | 21 | ### `/src/` - Source Code 22 | - **`main.rs`** - Main application entry point 23 | - **`platform.rs`** - Cross-platform compatibility layer 24 | - **`ui.rs`** - User interface and progress bars 25 | - **`media_cleaning.rs`** - File cleaning and organization 26 | - **`metadata_extraction.rs`** - Metadata extraction from JSON 27 | - **`metadata_embed.rs`** - Embedding metadata into files 28 | - **`sort_to_folders.rs`** - File sorting and folder creation 29 | - **`csv_report.rs`** - CSV report generation 30 | - **`html_report.rs`** - HTML report generation 31 | - **`filename_date_guess.rs`** - Date extraction from filenames 32 | - **`utils.rs`** - Utility functions 33 | 34 | ### `/assets/` - Resources 35 | - **`logo.png`** - MetaSort logo 36 | - **`upi.png`** - UPI QR code for donations 37 | 38 | ### `/target/` - Build Output 39 | - Compiled executables and build artifacts 40 | 41 | ## 🚀 How to Use 42 | 43 | ### For Non-Technical Users: 44 | 1. **Double-click `Run_MetaSort.command`** - Opens MetaSort in a new terminal window 45 | 2. Follow the prompts to organize your photos 46 | 47 | ### For Developers: 48 | 1. **Build**: `./scripts/build_macos.sh` (macOS) or `./scripts/build_windows.bat` (Windows) 49 | 2. **Run**: `cargo run --release` 50 | 3. **Install dependencies**: See `docs/SIMPLE_INSTALL.md` 51 | 52 | ## 🎯 Key Features 53 | - ✅ **Cross-platform** - Works on macOS and Windows 54 | - ✅ **User-friendly** - Multiple launcher options 55 | - ✅ **Clean organization** - Well-structured codebase 56 | - ✅ **Easy installation** - Automated scripts for both platforms 57 | - ✅ **Comprehensive docs** - Separate guides for different user types -------------------------------------------------------------------------------- /docs/SIMPLE_INSTALL.md: -------------------------------------------------------------------------------- 1 | # 🚀 Simple Installation Guide for Non-Technical Users 2 | 3 | ## macOS Users 4 | 5 | ### Step 1: Install Dependencies 6 | Open Terminal and run these commands one by one: 7 | 8 | ```bash 9 | # Install Homebrew (if you don't have it) 10 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 11 | 12 | # Install Rust 13 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 14 | # Press 1 when prompted, then restart Terminal 15 | 16 | # Install ExifTool 17 | brew install exiftool 18 | ``` 19 | 20 | ### Step 2: Download and Build MetaSort 21 | ```bash 22 | # Download MetaSort 23 | git clone https://github.com/iamsanmith/MetaSort.git 24 | cd MetaSort_v1.0.0 25 | 26 | # Build and create launchers 27 | ./scripts/build_macos.sh 28 | ``` 29 | 30 | ### Step 3: Use MetaSort 31 | After building, you'll have two easy ways to run MetaSort: 32 | 33 | 1. **Double-click `Run_MetaSort.command`** (recommended) 34 | - Opens MetaSort in a new terminal window 35 | - Perfect for non-technical users 36 | 37 | 2. **Double-click `MetaSort.command`** 38 | - Runs MetaSort in the current terminal 39 | - For advanced users 40 | 41 | ## Windows Users 42 | 43 | ### Step 1: Quick Installation 44 | 1. Download MetaSort from GitHub 45 | 2. Right-click on `scripts/install_windows.bat` and select "Run as administrator" 46 | 3. Follow the prompts 47 | 48 | ### Step 2: Manual Installation (if quick install fails) 49 | 1. Install Rust from https://rustup.rs/ 50 | 2. Install ExifTool using winget: `winget install ExifTool.ExifTool` 51 | 3. Open Command Prompt in the MetaSort folder 52 | 4. Run: `cargo build --release` 53 | 5. Run: `cargo run --release` 54 | 55 | ## 🎯 What MetaSort Does 56 | 57 | MetaSort helps you organize your Google Photos Takeout (or any messy photo folder) by: 58 | 59 | - 🧹 Cleaning up file names 60 | - 📅 Sorting photos by date 61 | - 🏷️ Adding missing metadata 62 | - 📊 Creating beautiful reports 63 | - 📁 Organizing everything into neat folders 64 | 65 | ## 🆘 Troubleshooting 66 | 67 | ### "ExifTool not found" Error 68 | - **macOS**: Run `brew install exiftool` in Terminal 69 | - **Windows**: Run `winget install ExifTool.ExifTool` in Command Prompt 70 | 71 | ### "Rust not found" Error 72 | - **macOS**: Run `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` 73 | - **Windows**: Download and install from https://rustup.rs/ 74 | 75 | ### App won't open on macOS 76 | - Right-click the app → Open 77 | - Or use the `.command` files instead 78 | 79 | ## 📞 Need Help? 80 | 81 | If you're still having trouble: 82 | 1. Check that all dependencies are installed 83 | 2. Make sure you're running the commands in the correct directory 84 | 3. Try the alternative launcher options 85 | 4. Check the main README.md for detailed instructions -------------------------------------------------------------------------------- /scripts/install_windows.ps1: -------------------------------------------------------------------------------- 1 | # MetaSort Windows Installation Helper (PowerShell) 2 | Write-Host "========================================" -ForegroundColor Cyan 3 | Write-Host "MetaSort Windows Installation Helper" -ForegroundColor Cyan 4 | Write-Host "========================================" -ForegroundColor Cyan 5 | Write-Host "" 6 | 7 | # Check if Rust is installed 8 | Write-Host "Checking if Rust is installed..." -ForegroundColor Yellow 9 | try { 10 | $rustVersion = rustc --version 2>$null 11 | if ($LASTEXITCODE -eq 0) { 12 | Write-Host "✅ Rust is installed: $rustVersion" -ForegroundColor Green 13 | } else { 14 | throw "Rust not found" 15 | } 16 | } catch { 17 | Write-Host "❌ Rust is not installed." -ForegroundColor Red 18 | Write-Host "" 19 | Write-Host "Please install Rust from: https://rustup.rs/" -ForegroundColor Yellow 20 | Write-Host "After installation, restart this script." -ForegroundColor Yellow 21 | Read-Host "Press Enter to exit" 22 | exit 1 23 | } 24 | 25 | Write-Host "" 26 | Write-Host "Checking if ExifTool is installed..." -ForegroundColor Yellow 27 | try { 28 | $exifVersion = exiftool -ver 2>$null 29 | if ($LASTEXITCODE -eq 0) { 30 | Write-Host "✅ ExifTool is installed: $exifVersion" -ForegroundColor Green 31 | } else { 32 | throw "ExifTool not found" 33 | } 34 | } catch { 35 | Write-Host "❌ ExifTool is not installed." -ForegroundColor Red 36 | Write-Host "" 37 | Write-Host "Attempting to install ExifTool..." -ForegroundColor Yellow 38 | 39 | # Try winget first 40 | Write-Host "Trying winget installation..." -ForegroundColor Yellow 41 | try { 42 | winget install ExifTool.ExifTool 43 | if ($LASTEXITCODE -eq 0) { 44 | Write-Host "✅ ExifTool installed successfully via winget!" -ForegroundColor Green 45 | } else { 46 | throw "Winget installation failed" 47 | } 48 | } catch { 49 | Write-Host "Winget installation failed. Trying Chocolatey..." -ForegroundColor Yellow 50 | try { 51 | choco install exiftool -y 52 | if ($LASTEXITCODE -eq 0) { 53 | Write-Host "✅ ExifTool installed successfully via Chocolatey!" -ForegroundColor Green 54 | } else { 55 | throw "Chocolatey installation failed" 56 | } 57 | } catch { 58 | Write-Host "❌ Automatic installation failed." -ForegroundColor Red 59 | Write-Host "" 60 | Write-Host "Please install ExifTool manually:" -ForegroundColor Yellow 61 | Write-Host "1. Download from https://exiftool.org/" -ForegroundColor White 62 | Write-Host "2. Extract to C:\exiftool" -ForegroundColor White 63 | Write-Host "3. Add C:\exiftool to your PATH environment variable" -ForegroundColor White 64 | Write-Host "4. Restart your PowerShell session" -ForegroundColor White 65 | Read-Host "Press Enter to exit" 66 | exit 1 67 | } 68 | } 69 | 70 | Write-Host "" 71 | Write-Host "⚠️ Please restart your PowerShell session for PATH changes to take effect." -ForegroundColor Yellow 72 | Write-Host "Then run this script again to continue." -ForegroundColor Yellow 73 | Read-Host "Press Enter to exit" 74 | exit 0 75 | } 76 | 77 | Write-Host "" 78 | Write-Host "Building MetaSort..." -ForegroundColor Yellow 79 | try { 80 | cargo build --release 81 | if ($LASTEXITCODE -eq 0) { 82 | Write-Host "✅ MetaSort built successfully!" -ForegroundColor Green 83 | } else { 84 | throw "Build failed" 85 | } 86 | } catch { 87 | Write-Host "❌ Build failed. Please check the error messages above." -ForegroundColor Red 88 | Read-Host "Press Enter to exit" 89 | exit 1 90 | } 91 | 92 | Write-Host "" 93 | Write-Host "🎉 Installation complete!" -ForegroundColor Green 94 | Write-Host "" 95 | Write-Host "You can now run MetaSort with:" -ForegroundColor Cyan 96 | Write-Host "cargo run --release" -ForegroundColor White 97 | Write-Host "" 98 | Read-Host "Press Enter to exit" -------------------------------------------------------------------------------- /src/metadata_embed.rs: -------------------------------------------------------------------------------- 1 | // metadata_embed.rs 2 | // Embedding metadata logic for MetaSort_v1.0.0 – Google Photos Takeout Organizer 3 | 4 | use std::fs::{self, File}; 5 | use std::io::{self, Write}; 6 | use std::path::Path; 7 | use crate::metadata_extraction::MediaMetadata; 8 | use crate::filename_date_guess::extract_date_from_filename; 9 | use crate::utils::log_to_file; 10 | use crate::platform::get_exiftool_command; 11 | 12 | pub fn embed_metadata_all(metadata_list: &[MediaMetadata], log_dir: &Path) { 13 | let logs_dir = log_dir.join("logs"); 14 | let log_path = logs_dir.join("metadata_embedding.log"); 15 | let _ = fs::create_dir_all(&logs_dir); 16 | let _log_file = File::create(&log_path).expect("Failed to create log file"); 17 | println!("\n🧐Do you want to embed date/time for WhatsApp & Screenshot images based on their \n1. Metadata\n2. Filename\n"); 18 | let mut input = String::new(); 19 | io::stdin().read_line(&mut input).expect("Failed to read line"); 20 | let use_filename = matches!(input.trim(), "2"); 21 | let total = metadata_list.len(); 22 | let mut processed = 0; 23 | for meta in metadata_list { 24 | let mut args = Vec::new(); 25 | let filename = meta.media_path.file_name().and_then(|n| n.to_str()).unwrap_or(""); 26 | let parent = meta.media_path.parent().and_then(|p| p.file_name()).and_then(|n| n.to_str()).unwrap_or(""); 27 | let is_wa = parent.eq_ignore_ascii_case("Whatsapp"); 28 | let is_sc = parent.eq_ignore_ascii_case("Screenshots"); 29 | let mut used = "metadata"; 30 | let mut date_to_embed = meta.exif_date.clone(); 31 | if use_filename && (is_wa || is_sc) { 32 | if let Some(date) = extract_date_from_filename(filename) { 33 | date_to_embed = Some(date); 34 | used = "filename"; 35 | } 36 | } 37 | if date_to_embed.is_none() { 38 | used = "metadata (fallback)"; 39 | } 40 | if let Some(ref date) = date_to_embed { 41 | if meta.media_path.extension().map(|e| e.to_ascii_lowercase()) == Some("png".into()) { 42 | args.push(format!("-XMP:DateTimeOriginal={}", date)); 43 | } else { 44 | args.push(format!("-DateTimeOriginal={}", date)); 45 | } 46 | } 47 | if let (Some(lat), Some(lon)) = (meta.gps_latitude, meta.gps_longitude) { 48 | args.push(format!("-GPSLatitude={}", lat)); 49 | args.push(format!("-GPSLongitude={}", lon)); 50 | } 51 | if let Some(alt) = meta.gps_altitude { 52 | args.push(format!("-GPSAltitude={}", alt)); 53 | } 54 | if let Some(ref make) = meta.camera_make { 55 | args.push(format!("-Make={}", make)); 56 | } 57 | if let Some(ref model) = meta.camera_model { 58 | args.push(format!("-Model={}", model)); 59 | } 60 | // Add more fields as needed 61 | args.push("-overwrite_original".to_string()); 62 | args.push(meta.media_path.to_string_lossy().to_string()); 63 | let log_msg = format!( 64 | "File: {:?}, Used: {}, Date: {:?}, Lat: {:?}, Lon: {:?}, Alt: {:?}, Make: {:?}, Model: {:?}", 65 | meta.media_path.file_name().unwrap_or_default(), used, date_to_embed, meta.gps_latitude, meta.gps_longitude, meta.gps_altitude, meta.camera_make, meta.camera_model 66 | ); 67 | let status = get_exiftool_command() 68 | .args(&args) 69 | .status(); 70 | if let Ok(status) = status { 71 | if status.success() { 72 | log_to_file(&logs_dir, "metadata_embedding.log", &format!("✅ Embedded metadata. {}", log_msg)); 73 | } else { 74 | log_to_file(&logs_dir, "metadata_embedding.log", &format!("❌ Failed to embed metadata. {}", log_msg)); 75 | } 76 | } else { 77 | log_to_file(&logs_dir, "metadata_embedding.log", &format!("❌ Error running exiftool. {}", log_msg)); 78 | } 79 | processed += 1; 80 | print_progress(processed, total); 81 | } 82 | println!("\n✅ Metadata embedding complete! Embedded metadata for {} files. Log: {:?}", processed, log_path); 83 | } 84 | 85 | fn print_progress(done: usize, total: usize) { 86 | let percent = if total > 0 { (done * 100) / total } else { 100 }; 87 | let bar = format!("{}{}", "🟦".repeat(percent / 4), "⬜".repeat(25 - percent / 4)); 88 | print!("\r✍️ Embedding metadata: [{}] {}% ({} / {})", bar, percent, done, total); 89 | let _ = std::io::stdout().flush(); 90 | if done == total { 91 | println!(); 92 | } 93 | } -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | // ui.rs 2 | // Polished terminal output for MetaSort 3 | 4 | use indicatif::{ProgressBar, ProgressStyle}; 5 | use std::time::Duration; 6 | 7 | pub struct MetaSortUI { 8 | main_progress: Option, 9 | } 10 | 11 | impl MetaSortUI { 12 | pub fn new() -> Self { 13 | Self { 14 | main_progress: None, 15 | } 16 | } 17 | 18 | pub fn print_header() { 19 | println!(r#" 20 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 21 | ┃ ┃ 22 | ┃ ███╗ ███╗███████╗████████╗ █████╗ ███████╗ ██████╗ ██████╗ ████████╗┃ 23 | ┃ ████╗ ████║██╔════╝╚══██╔══╝██╔══██╗██╔════╝██╔═══██╗██╔══██╗╚══██╔══╝┃ 24 | ┃ ██╔████╔██║█████╗ ██║ ███████║███████╗██║ ██║██████╔╝ ██║ ┃ 25 | ┃ ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║╚════██║██║ ██║██╔══██╗ ██║ ┃ 26 | ┃ ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║███████║╚██████╔╝██║ ██║ ██║ ┃ 27 | ┃ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ┃ 28 | ┃ ┃ 29 | ┃ MetaSort - Google Photos Takeout Organizer! ┃ 30 | ┃ ┃ 31 | ┃ Version 1.0 ┃ 32 | ┃ Developed by Sanmith S ┃ 33 | ┃ Cross-platform (macOS & Windows) ┃ 34 | ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 35 | "#); 36 | } 37 | 38 | pub fn print_section_header(title: &str) { 39 | println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 40 | println!(" {}", title); 41 | println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 42 | } 43 | 44 | pub fn print_success(message: &str) { 45 | println!("✅ {}", message); 46 | } 47 | 48 | pub fn print_warning(message: &str) { 49 | println!("⚠️ {}", message); 50 | } 51 | 52 | pub fn print_error(message: &str) { 53 | println!("❌ {}", message); 54 | } 55 | 56 | pub fn print_info(message: &str) { 57 | println!("ℹ️ {}", message); 58 | } 59 | 60 | pub fn start_main_progress(&mut self, total: u64, message: &str) { 61 | let pb = ProgressBar::new(total); 62 | pb.set_style( 63 | ProgressStyle::default_bar() 64 | .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}") 65 | .unwrap() 66 | .progress_chars("█░") 67 | ); 68 | pb.set_message(message.to_string()); 69 | pb.enable_steady_tick(Duration::from_millis(120)); 70 | self.main_progress = Some(pb); 71 | } 72 | 73 | pub fn finish_progress(&mut self, message: &str) { 74 | if let Some(pb) = &self.main_progress { 75 | pb.finish_with_message(message.to_string()); 76 | } 77 | self.main_progress = None; 78 | } 79 | 80 | pub fn print_summary( 81 | photos: usize, 82 | videos: usize, 83 | whatsapp: usize, 84 | screenshots: usize, 85 | unknown: usize, 86 | mkv: usize, 87 | errors: usize, 88 | output_path: &str, 89 | ) { 90 | let total = photos + videos + whatsapp + screenshots + unknown + mkv; 91 | 92 | println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 93 | println!(" MetaSort Summary"); 94 | println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 95 | println!(" 📸 Photos processed: {}", photos); 96 | println!(" 🎥 Videos processed: {}", videos); 97 | println!(" 💬 WhatsApp images: {}", whatsapp); 98 | println!(" 📱 Screenshots: {}", screenshots); 99 | println!(" ❓ Unknown time: {}", unknown); 100 | println!(" 🎬 MKV files: {}", mkv); 101 | println!(" 📊 Total files: {}", total); 102 | println!(" ⚠️ Errors encountered: {}", errors); 103 | println!(" 📁 Output location: {}", output_path); 104 | println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 105 | } 106 | 107 | pub fn print_footer() { 108 | println!("\n"); 109 | println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 110 | println!(" 💖 Like my work? Please consider donating! 💖"); 111 | println!(" 🌟 Support MetaSort: https://upier.vercel.app/pay/sanmith@superyes"); 112 | println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 113 | } 114 | } -------------------------------------------------------------------------------- /docs/CROSS_PLATFORM_CHANGES.md: -------------------------------------------------------------------------------- 1 | # Cross-Platform Changes for MetaSort v1.0.0 2 | 3 | ## Overview 4 | MetaSort has been updated to support both macOS and Windows platforms, with Linux support planned for future versions. 5 | 6 | ## Changes Made 7 | 8 | ### 1. New Platform Module (`src/platform.rs`) 9 | - **Cross-platform exiftool command detection**: Uses `exiftool.exe` on Windows, `exiftool` on other platforms 10 | - **ExifTool availability checking**: Verifies if exiftool is installed and accessible 11 | - **Platform-specific installation instructions**: Provides tailored setup instructions for each OS 12 | - **Utility functions**: Path handling and platform detection helpers 13 | 14 | ### 2. Updated Main Application (`src/main.rs`) 15 | - **ExifTool validation**: Checks for exiftool availability at startup 16 | - **User-friendly error messages**: Provides clear installation instructions if exiftool is missing 17 | - **Cross-platform branding**: Updated ASCII art to mention cross-platform support 18 | 19 | ### 3. Updated Core Modules 20 | - **`src/metadata_embed.rs`**: Now uses platform-specific exiftool command 21 | - **`src/sort_to_folders.rs`**: Updated to use platform-specific exiftool command 22 | - **Removed unused imports**: Cleaned up compilation warnings 23 | 24 | ### 4. Windows Installation Scripts 25 | - **`install_windows.ps1`**: PowerShell script with colored output and comprehensive error handling 26 | - **`install_windows.bat`**: Batch script for basic installation 27 | - **Automatic dependency management**: Attempts to install exiftool via winget or Chocolatey 28 | - **User guidance**: Clear instructions for manual installation if automatic methods fail 29 | 30 | ### 5. Updated Documentation (`README.md`) 31 | - **Windows installation section**: Comprehensive setup instructions 32 | - **Multiple installation methods**: winget, Chocolatey, and manual installation options 33 | - **Quick start scripts**: Documentation for the new installation helpers 34 | - **Cross-platform branding**: Updated descriptions to mention Windows support 35 | 36 | ### 6. Updated Package Configuration (`Cargo.toml`) 37 | - **Updated description**: Now mentions cross-platform support 38 | - **Added comments**: Notes about cross-platform compatibility 39 | 40 | ## Platform-Specific Features 41 | 42 | ### Windows 43 | - **ExifTool command**: `exiftool.exe` 44 | - **Installation methods**: winget, Chocolatey, manual PATH setup 45 | - **Path handling**: Backslash separators 46 | - **Installation scripts**: PowerShell and batch file helpers 47 | 48 | ### macOS 49 | - **ExifTool command**: `exiftool` 50 | - **Installation method**: Homebrew (`brew install exiftool`) 51 | - **Path handling**: Forward slash separators 52 | 53 | ### Linux (Future) 54 | - **ExifTool command**: `exiftool` 55 | - **Installation methods**: apt, yum, dnf package managers 56 | - **Path handling**: Forward slash separators 57 | 58 | ## User Experience Improvements 59 | 60 | ### Before (macOS only) 61 | - Users had to manually install exiftool via Homebrew 62 | - No validation of exiftool availability 63 | - Platform-specific hardcoded commands 64 | - Limited installation guidance 65 | 66 | ### After (Cross-platform) 67 | - **Automatic validation**: ExifTool availability checked at startup 68 | - **Clear error messages**: Platform-specific installation instructions 69 | - **Installation helpers**: Automated setup scripts for Windows 70 | - **Multiple installation options**: Various methods to install dependencies 71 | - **Better user guidance**: Step-by-step instructions for each platform 72 | 73 | ## Testing Recommendations 74 | 75 | ### Windows Testing 76 | 1. Test on Windows 10/11 with PowerShell 77 | 2. Verify winget installation method 78 | 3. Verify Chocolatey installation method 79 | 4. Test manual PATH setup 80 | 5. Verify exiftool.exe command detection 81 | 82 | ### macOS Testing 83 | 1. Verify existing Homebrew installation still works 84 | 2. Test exiftool availability checking 85 | 3. Ensure no regression in functionality 86 | 87 | ### Cross-Platform Testing 88 | 1. Verify path handling differences 89 | 2. Test file operations on different platforms 90 | 3. Ensure consistent behavior across platforms 91 | 92 | ## Future Enhancements 93 | 94 | ### Planned Features 95 | - **Linux support**: Full Linux compatibility 96 | - **GUI interface**: Cross-platform GUI using Tauri or similar 97 | - **Docker support**: Containerized version for consistent environments 98 | - **CI/CD**: Automated testing across all platforms 99 | 100 | ### Potential Improvements 101 | - **Native exiftool integration**: Embed exiftool binary or use Rust libraries 102 | - **Platform-specific optimizations**: Tailored performance improvements 103 | - **Advanced installation**: More sophisticated dependency management 104 | 105 | ## Migration Notes 106 | 107 | ### For Existing Users 108 | - **No breaking changes**: All existing functionality preserved 109 | - **Enhanced experience**: Better error messages and validation 110 | - **Optional features**: New installation scripts are optional 111 | 112 | ### For Developers 113 | - **New module**: `src/platform.rs` contains cross-platform utilities 114 | - **Updated imports**: Some modules now use platform-specific functions 115 | - **Testing**: Consider testing on multiple platforms 116 | 117 | ## Conclusion 118 | 119 | MetaSort is now a truly cross-platform application that provides a consistent experience across macOS and Windows. The changes maintain backward compatibility while adding robust platform detection, improved error handling, and user-friendly installation processes. 120 | 121 | The addition of Windows support significantly expands MetaSort's user base and makes it accessible to a wider audience of users who need to organize their Google Photos takeout data. -------------------------------------------------------------------------------- /src/html_report.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::Write; 3 | use std::path::Path; 4 | use std::path::PathBuf; 5 | use url::Url; 6 | 7 | pub fn generate_html_report( 8 | output_dir: &Path, 9 | total: usize, 10 | photos: usize, 11 | videos: usize, 12 | whatsapp: usize, 13 | screenshots: usize, 14 | unknown: usize, 15 | mkv: usize, 16 | errors: usize, 17 | csv_files: &[&str], 18 | log_files: &[&str], 19 | metadata_fields: &[&str], 20 | ) { 21 | let html_path = output_dir.join("MetaSort_Summary.html"); 22 | let mut file = File::create(&html_path).expect("Failed to create HTML report"); 23 | 24 | // Helper to get file:// URL 25 | fn file_url(path: &PathBuf) -> String { 26 | let abs = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()); 27 | Url::from_file_path(&abs).unwrap().to_string() 28 | } 29 | 30 | let csv_links = csv_files.iter().map(|f| { 31 | let path = output_dir.join("Technical Files").join("CSV Report").join(f); 32 | let url = file_url(&path); 33 | format!("
  • {}
  • ", url, f) 34 | }).collect::(); 35 | let log_links = log_files.iter().map(|f| { 36 | let path = output_dir.join("Technical Files").join("logs").join(f); 37 | let url = file_url(&path); 38 | format!("
  • {}
  • ", url, f) 39 | }).collect::(); 40 | let meta_links = metadata_fields.iter().map(|f| format!("
  • {}
  • ", f)).collect::(); 41 | 42 | let html = format!("\ 43 | \ 44 | \ 45 | \ 46 | \ 47 | MetaSort Summary Report\ 48 | \ 49 | \ 69 | \ 70 | \ 71 |
    \ 72 | MetaSort Logo\ 73 |

    📊 MetaSort Summary Report

    \ 74 |
    Your Google Photos Takeout, beautifully organized! ✨
    \ 75 |
    \ 76 |
    \ 77 |
    Click a file to open it in your default app or reveal it in Finder.
    \ 78 | \ 79 | \ 80 | \ 81 | \ 82 | \ 83 | \ 84 | \ 85 | \ 86 | \ 87 |
    📦Total Files Processed{}
    🖼️Photos{}
    🎬Videos{}
    💬WhatsApp Images{}
    📱Screenshots{}
    Unknown Time{}
    🎞️MKV Files{}
    ⚠️Errors{}
    \ 88 |
    📑CSV Reports
    \ 89 |
      {}
    \ 90 |
    📝Log Files
    \ 91 |
      {}
    \ 92 |
    🔍Metadata Fields Extracted/Embedded
    \ 93 |
      {}
    \ 94 |
    \ 95 | \ 98 | \ 99 | \ 100 | ", 101 | total, photos, videos, whatsapp, screenshots, unknown, mkv, errors, 102 | csv_links, log_links, meta_links 103 | ); 104 | let _ = writeln!(file, "{}", html); 105 | println!("\n📄 HTML summary report written to: {:?}", html_path); 106 | } -------------------------------------------------------------------------------- /src/media_cleaning.rs: -------------------------------------------------------------------------------- 1 | // json_clean.rs 2 | // JSON renaming/cleaning logic for MetaSort_v1.0.0 – Google Photos Takeout Organizer 3 | 4 | use std::fs; 5 | use std::path::Path; 6 | use walkdir::WalkDir; 7 | use regex::Regex; 8 | use std::io::{self, Write}; 9 | use crate::utils::log_to_file; 10 | 11 | pub fn ask_and_separate_whatsapp_screenshots(base_path: &str, separate_wa_sc: bool) { 12 | if !separate_wa_sc { 13 | return; 14 | } 15 | let logs_dir = Path::new(base_path).join("logs"); 16 | log_to_file(&logs_dir, "media_cleaning.log", "User chose to separate WhatsApp and Screenshot images."); 17 | let whatsapp_patterns = vec![ 18 | Regex::new(r"(?i)^(IMG-\d{8}-WA\d+|IMG-WA\d+|WA\d+|VID-\d{8}-WA\d+|VID-WA\d+|WhatsApp Image \d{4}-\d{2}-\d{2} at \d{2}\.\d{2}\.\d{2}|WhatsApp Video \d{4}-\d{2}-\d{2} at \d{2}\.\d{2}\.\d{2})").unwrap(), 19 | ]; 20 | let screenshot_patterns = vec![ 21 | Regex::new(r"(?i)^(Screenshot(_| )?\d{4}-\d{2}-\d{2}(-| )?\d{2}(-|\.|:)?\d{2}(-|\.|:)?\d{2}|Screenshot \(\d+\)|Screen Shot \d{4}-\d{2}-\d{2} at \d{2}\.\d{2}\.\d{2}|Screenshot_\d+|Screenshot_\d{8}-\d{6}|スクリーンショット|Снимок экрана|Captura de pantalla|Capture d'écran|Bildschirmfoto|Istantanea|Skjermbilde|Skärmbild|Ekran görüntüsü|Zrzut ekranu|PrtSc|Snip)").unwrap(), 22 | ]; 23 | let other_images_dir = Path::new(base_path).join("Other Images"); 24 | let whatsapp_dir = other_images_dir.join("Whatsapp"); 25 | let screenshots_dir = other_images_dir.join("Screenshots"); 26 | let _ = fs::create_dir_all(&whatsapp_dir); 27 | let _ = fs::create_dir_all(&screenshots_dir); 28 | let all_files: Vec<_> = WalkDir::new(base_path).into_iter().filter_map(Result::ok).filter(|e| e.path().is_file()).collect(); 29 | let total = all_files.len(); 30 | let mut processed = 0; 31 | for entry in all_files { 32 | let path = entry.path(); 33 | if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { 34 | // WhatsApp 35 | if whatsapp_patterns.iter().any(|re| re.is_match(filename)) { 36 | let dest = whatsapp_dir.join(filename); 37 | let _ = fs::rename(path, &dest); 38 | log_to_file(&logs_dir, "media_cleaning.log", &format!("Moved WhatsApp image {:?} to {:?}", path, dest)); 39 | // Move .json if exists 40 | let json_path = path.with_extension(format!("{}.json", path.extension().and_then(|e| e.to_str()).unwrap_or(""))); 41 | if json_path.exists() { 42 | let json_dest = whatsapp_dir.join(json_path.file_name().unwrap()); 43 | let _ = fs::rename(&json_path, &json_dest); 44 | log_to_file(&logs_dir, "media_cleaning.log", &format!("Moved WhatsApp JSON {:?} to {:?}", json_path, json_dest)); 45 | } 46 | processed += 1; 47 | print_progress(processed, total, path); 48 | continue; 49 | } 50 | // Screenshot 51 | if screenshot_patterns.iter().any(|re| re.is_match(filename)) { 52 | let dest = screenshots_dir.join(filename); 53 | let _ = fs::rename(path, &dest); 54 | log_to_file(&logs_dir, "media_cleaning.log", &format!("Moved Screenshot image {:?} to {:?}", path, dest)); 55 | // Move .json if exists 56 | let json_path = path.with_extension(format!("{}.json", path.extension().and_then(|e| e.to_str()).unwrap_or(""))); 57 | if json_path.exists() { 58 | let json_dest = screenshots_dir.join(json_path.file_name().unwrap()); 59 | let _ = fs::rename(&json_path, &json_dest); 60 | log_to_file(&logs_dir, "media_cleaning.log", &format!("Moved Screenshot JSON {:?} to {:?}", json_path, json_dest)); 61 | } 62 | processed += 1; 63 | print_progress(processed, total, path); 64 | } 65 | } 66 | } 67 | println!("\n🧹 WhatsApp/Screenshot separation complete! Processed {} files.", processed); 68 | } 69 | 70 | fn print_progress(done: usize, total: usize, file: &Path) { 71 | let percent = if total > 0 { (done * 100) / total } else { 100 }; 72 | let bar = "🧹".repeat(percent / 4); 73 | let fname = file.file_name().and_then(|n| n.to_str()).unwrap_or(""); 74 | print!("\r🧹 Cleaning: {} {}% ({}/{}) | {}", bar, percent, done, total, fname); 75 | let _ = io::stdout().flush(); 76 | if done == total { 77 | println!(); 78 | } 79 | } 80 | 81 | pub fn clean_json_filenames(base_path: &str) { 82 | let media_extensions = vec![ 83 | // Images 84 | "jpg", "jpeg", "png", "webp", "heic", "heif", "bmp", "tiff", "gif", "avif", "jxl", "jfif", 85 | // Videos 86 | "mp4", "mov", "mkv", "avi", "webm", "3gp", "m4v", "mpg", "mpeg", "mts", "m2ts", "ts", "flv", 87 | "f4v", "wmv", "asf", "rm", "rmvb", "vob", "ogv", "mxf", "dv", "divx", "xvid" 88 | ]; 89 | 90 | let temp_dir = Path::new(base_path).join("`MetaSort_temp"); 91 | let _ = fs::create_dir_all(&temp_dir); 92 | let log_path = temp_dir.join("rename_log.txt"); 93 | let mut log_file = fs::File::create(&log_path).expect("Failed to create log file"); 94 | 95 | for entry in WalkDir::new(base_path).into_iter().filter_map(Result::ok) { 96 | let path = entry.path(); 97 | if path.is_file() { 98 | if let Some(ext) = path.extension().and_then(|e| e.to_str()) { 99 | let ext_lc = ext.to_lowercase(); 100 | if media_extensions.contains(&ext_lc.as_str()) { 101 | let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); 102 | let parent = path.parent().unwrap_or(Path::new("")); 103 | if let Ok(entries) = fs::read_dir(parent) { 104 | for json_entry in entries { 105 | if let Ok(json_file) = json_entry { 106 | let json_path = json_file.path(); 107 | if json_path.is_file() { 108 | if let Some(json_name) = json_path.file_name().and_then(|n| n.to_str()) { 109 | if json_name.starts_with(filename) && json_name.ends_with(".json") && json_name != format!("{}.json", filename) { 110 | let new_json_path = parent.join(format!("{}.json", filename)); 111 | if let Err(e) = fs::rename(&json_path, &new_json_path) { 112 | let _ = log_file.write_all(format!("❌ Failed to rename {:?} to {:?}: {}\n", json_path, new_json_path, e).as_bytes()); 113 | } else { 114 | let _ = log_file.write_all(format!("✅ Renamed JSON {:?} to {:?}\n", json_path, new_json_path).as_bytes()); 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | let summary = format!("\n🧹 JSON filename cleaning complete.\n"); 127 | let _ = log_file.write_all(summary.as_bytes()); 128 | } -------------------------------------------------------------------------------- /src/metadata_extraction.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::{Path, PathBuf}; 3 | use walkdir::WalkDir; 4 | use serde_json::Value; 5 | use chrono::{TimeZone, Utc}; 6 | use crate::utils::log_to_file; 7 | use std::io; 8 | use std::io::Write; 9 | use crate::filename_date_guess::extract_date_from_filename; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct MediaMetadata { 13 | pub media_path: PathBuf, 14 | pub _json_path: PathBuf, 15 | pub exif_date: Option, 16 | pub gps_latitude: Option, 17 | pub gps_longitude: Option, 18 | pub gps_altitude: Option, 19 | pub camera_make: Option, 20 | pub camera_model: Option, 21 | } 22 | 23 | pub fn extract_metadata(base_path: &str) -> (Vec, Vec) { 24 | let mut media_json_pairs: Vec<(PathBuf, PathBuf)> = Vec::new(); 25 | let mut all_media_files: Vec = Vec::new(); 26 | let media_extensions = vec![ 27 | // Images 28 | "jpg", "jpeg", "png", "webp", "heic", "heif", "bmp", "tiff", "gif", "avif", "jxl", "jfif", 29 | "raw", "cr2", "nef", "orf", "sr2", "arw", "dng", "pef", "raf", "rw2", "srw", "3fr", "erf", 30 | "k25", "kdc", "mef", "mos", "mrw", "nrw", "srf", "x3f", "svg", "ico", "psd", "ai", "eps", 31 | // Videos 32 | "mp4", "mov", "mkv", "avi", "webm", "3gp", "m4v", "mpg", "mpeg", "mts", "m2ts", "ts", "flv", 33 | "f4v", "wmv", "asf", "rm", "rmvb", "vob", "ogv", "mxf", "dv", "divx", "xvid" 34 | ]; 35 | 36 | let logs_dir = Path::new(base_path).join("logs"); 37 | 38 | // Find all media files and their matching .json 39 | for entry in WalkDir::new(base_path).into_iter().filter_map(Result::ok) { 40 | let path = entry.path(); 41 | if path.is_file() { 42 | if let Some(ext) = path.extension().and_then(|e| e.to_str()) { 43 | let ext_lc = ext.to_lowercase(); 44 | if media_extensions.contains(&ext_lc.as_str()) { 45 | all_media_files.push(path.to_path_buf()); 46 | let json_path = path.with_extension(format!("{}.json", ext_lc)); 47 | let json_path_alt = path.with_extension(format!("{}", "json")); 48 | // Try both: IMG_001.JPG.json and IMG_001.jpg.json 49 | if json_path.exists() { 50 | media_json_pairs.push((path.to_path_buf(), json_path)); 51 | } else if json_path_alt.exists() { 52 | media_json_pairs.push((path.to_path_buf(), json_path_alt)); 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | let paired_media: Vec = media_json_pairs.iter().map(|(m, _)| m.clone()).collect(); 60 | let unpaired_media: Vec = all_media_files.into_iter().filter(|m| !paired_media.contains(m)).collect(); 61 | let mut metadata_list = Vec::new(); 62 | let mut failed_guess_paths = Vec::new(); 63 | let total = media_json_pairs.len(); 64 | let mut processed = 0; 65 | for (media_path, json_path) in &media_json_pairs { 66 | let json_str = match fs::read_to_string(json_path) { 67 | Ok(s) => s, 68 | Err(e) => { 69 | log_to_file(&logs_dir, "metadata_extraction.log", &format!("Failed to read JSON for {:?}: {}", json_path, e)); 70 | continue; 71 | } 72 | }; 73 | let v: Value = match serde_json::from_str(&json_str) { 74 | Ok(val) => val, 75 | Err(e) => { 76 | log_to_file(&logs_dir, "metadata_extraction.log", &format!("Failed to parse JSON for {:?}: {}", json_path, e)); 77 | continue; 78 | } 79 | }; 80 | // Extract timestamp and convert to EXIF format (original date only) 81 | let exif_date = v["photoTakenTime"]["timestamp"].as_str().and_then(|ts| { 82 | ts.parse::().ok().map(|timestamp| { 83 | let dt = Utc.timestamp_opt(timestamp, 0).unwrap(); 84 | dt.format("%Y:%m:%d %H:%M:%S").to_string() 85 | }) 86 | }); 87 | // Extract GPS 88 | let gps_latitude = v["geoData"]["latitude"].as_f64() 89 | .or_else(|| v["geoDataExif"]["latitude"].as_f64()); 90 | let gps_longitude = v["geoData"]["longitude"].as_f64() 91 | .or_else(|| v["geoDataExif"]["longitude"].as_f64()); 92 | let gps_altitude = v["geoData"]["altitude"].as_f64() 93 | .or_else(|| v["geoDataExif"]["altitude"].as_f64()); 94 | // Camera make/model 95 | let camera_make = v["cameraMake"].as_str().map(|s| s.to_string()); 96 | let camera_model = v["cameraModel"].as_str().map(|s| s.to_string()); 97 | 98 | metadata_list.push(MediaMetadata { 99 | media_path: media_path.clone(), 100 | _json_path: json_path.clone(), 101 | exif_date, 102 | gps_latitude, 103 | gps_longitude, 104 | gps_altitude, 105 | camera_make, 106 | camera_model, 107 | }); 108 | processed += 1; 109 | print_progress(processed, total); 110 | } 111 | // Handle unpaired media 112 | if !unpaired_media.is_empty() { 113 | println!( 114 | "\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:", 115 | unpaired_media.len(), paired_media.len() + unpaired_media.len(), (unpaired_media.len() * 100) / (paired_media.len() + unpaired_media.len()) 116 | ); 117 | let mut input = String::new(); 118 | io::stdin().read_line(&mut input).unwrap(); 119 | let guess = input.trim() == "2"; 120 | for media_path in unpaired_media { 121 | let filename = media_path.file_name().and_then(|n| n.to_str()).unwrap_or(""); 122 | let exif_date = if guess { 123 | if let Some(date) = extract_date_from_filename(filename) { 124 | log_to_file(&logs_dir, "metadata_extraction.log", &format!("Guessed date from filename for {:?}: {}", filename, date)); 125 | Some(date) 126 | } else { 127 | log_to_file(&logs_dir, "metadata_extraction.log", &format!("Could not guess date from filename for {:?}", filename)); 128 | failed_guess_paths.push(media_path.clone()); 129 | None 130 | } 131 | } else { 132 | log_to_file(&logs_dir, "metadata_extraction.log", &format!("No JSON for {:?}, moved to Unknown Time", filename)); 133 | None 134 | }; 135 | metadata_list.push(MediaMetadata { 136 | media_path: media_path.clone(), 137 | _json_path: PathBuf::new(), 138 | exif_date, 139 | gps_latitude: None, 140 | gps_longitude: None, 141 | gps_altitude: None, 142 | camera_make: None, 143 | camera_model: None, 144 | }); 145 | } 146 | } 147 | (metadata_list, failed_guess_paths) 148 | } 149 | 150 | fn print_progress(done: usize, total: usize) { 151 | let percent = if total > 0 { (done * 100) / total } else { 100 }; 152 | let bar = format!("{}{}", "🟩".repeat(percent / 4), "⬜".repeat(25 - percent / 4)); 153 | print!("\r🔍 Extracting metadata: [{}] {}% ({} / {})", bar, percent, done, total); 154 | let _ = std::io::stdout().flush(); 155 | if done == total { 156 | println!(); 157 | } 158 | } -------------------------------------------------------------------------------- /src/filename_date_guess.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | 3 | /// Attempts to extract a date/time from a filename using common patterns. 4 | /// Returns the date in EXIF format (YYYY:MM:DD HH:MM:SS) if found. 5 | pub fn extract_date_from_filename(filename: &str) -> Option { 6 | // WhatsApp: IMG-20220101-WA0001.jpg, WhatsApp Image 2022-01-01 at 12.34.56.jpg 7 | let wa1 = Regex::new(r"IMG-(\d{8})-WA\d+").unwrap(); 8 | let wa2 = Regex::new(r"WhatsApp Image (\d{4})-(\d{2})-(\d{2}) at (\d{2})\.(\d{2})\.(\d{2})").unwrap(); 9 | // Screenshot: Screenshot_2023-01-01-12-00-00.png, Screen Shot 2023-01-01 at 12.00.00.png 10 | let sc1 = Regex::new(r"Screenshot_(\d{4})-(\d{2})-(\d{2})-(\d{2})-(\d{2})-(\d{2})").unwrap(); 11 | let sc2 = Regex::new(r"Screen Shot (\d{4})-(\d{2})-(\d{2}) at (\d{2})\.(\d{2})\.(\d{2})").unwrap(); 12 | let sc3 = Regex::new(r"Screenshot_(\d{8})-(\d{6})").unwrap(); 13 | let tg1 = Regex::new(r"photo_(\d{4})-(\d{2})-(\d{2}) (\d{2})\.(\d{2})\.(\d{2})").unwrap(); 14 | let mi1 = Regex::new(r"IMG_(\d{8})_(\d{6})").unwrap(); 15 | let dt1 = Regex::new(r"(\d{4})-(\d{2})-(\d{2})-(\d{6})").unwrap(); 16 | let custom1 = Regex::new(r"[._-](\d{8})-(\d{4})").unwrap(); 17 | // Samsung, Google, Canon, Sony, etc. 18 | let samsung1 = Regex::new(r"(\\d{8})_(\\d{6})").unwrap(); // 20230101_123456 19 | let samsung2 = Regex::new(r"(\\d{4})-(\\d{2})-(\\d{2}) (\\d{2})\\.(\\d{2})\\.(\\d{2})").unwrap(); // 2023-01-01 12.34.56 20 | let samsung3 = Regex::new(r"(\\d{8})-(\\d{6})").unwrap(); // 20230101-123456 21 | let samsung4 = Regex::new(r"(\\d{4})-(\\d{2})-(\\d{2})_(\\d{2})-(\\d{2})-(\\d{2})").unwrap(); // 2023-01-01_12-34-56 22 | let samsung5 = Regex::new(r"(\\d{4})\\.(\\d{2})\\.(\\d{2})_(\\d{2})\\.(\\d{2})\\.(\\d{2})").unwrap(); // 2023.01.01_12.34.56 23 | let samsung6 = Regex::new(r"(\\d{4})_(\\d{2})_(\\d{2})_(\\d{2})_(\\d{2})_(\\d{2})").unwrap(); // 2023_01_01_12_34_56 24 | let pxl = Regex::new(r"PXL_(\\d{8})_(\\d{6,})").unwrap(); // PXL_20230101_123456789 25 | let ms1 = Regex::new(r"(\\d{4})-(\\d{2})-(\\d{2})_(\\d{2})-(\\d{2})-(\\d{2})-\\d+").unwrap(); // 2023-01-01_12-34-56-123 26 | let ms2 = Regex::new(r"Screenshot_(\\d{4})-(\\d{2})-(\\d{2})-(\\d{2})-(\\d{2})-(\\d{2})-\\d+").unwrap(); // Screenshot_2023-01-01-12-34-56-123 27 | let vid = Regex::new(r"VID_(\\d{8})_(\\d{6})").unwrap(); // VID_20230101_123456 28 | // Sony: DSC01234_20230101_123456.JPG, DSC_20230101_123456.JPG 29 | let sony1 = Regex::new(r"DSC\\d+_(\\d{8})_(\\d{6})").unwrap(); 30 | let sony2 = Regex::new(r"DSC_(\\d{8})_(\\d{6})").unwrap(); 31 | // Custom: RMLmc20250531_115820_RMlmc.7 32 | let rmlmc = Regex::new(r"RMLmc(\\d{8})_(\\d{6})").unwrap(); 33 | // Custom: wallpaper - IMG_20240113_143213Jan 13 2024 34 | let wallpaper = Regex::new(r"IMG_(\\d{8})_(\\d{6})Jan \\d{2} \\d{4}").unwrap(); 35 | // Custom: San-1 Oct 2024.jxl 36 | let san1 = Regex::new(r"(\\d{1,2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\\d{4})").unwrap(); 37 | // WhatsApp 38 | if let Some(caps) = wa1.captures(filename) { 39 | let date = &caps[1]; 40 | return Some(format!("{}:{}:{} 00:00:00", &date[0..4], &date[4..6], &date[6..8])); 41 | } 42 | if let Some(caps) = wa2.captures(filename) { 43 | return Some(format!("{}:{}:{} {}:{}:{}", &caps[1], &caps[2], &caps[3], &caps[4], &caps[5], &caps[6])); 44 | } 45 | // Screenshot 46 | if let Some(caps) = sc1.captures(filename) { 47 | return Some(format!("{}:{}:{} {}:{}:{}", &caps[1], &caps[2], &caps[3], &caps[4], &caps[5], &caps[6])); 48 | } 49 | if let Some(caps) = sc2.captures(filename) { 50 | return Some(format!("{}:{}:{} {}:{}:{}", &caps[1], &caps[2], &caps[3], &caps[4], &caps[5], &caps[6])); 51 | } 52 | if let Some(caps) = sc3.captures(filename) { 53 | let date = &caps[1]; 54 | let time = &caps[2]; 55 | return Some(format!("{}:{}:{} {}:{}:{}", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4], &time[4..6])); 56 | } 57 | // Telegram 58 | if let Some(caps) = tg1.captures(filename) { 59 | return Some(format!("{}:{}:{} {}:{}:{}", &caps[1], &caps[2], &caps[3], &caps[4], &caps[5], &caps[6])); 60 | } 61 | // MIUI 62 | if let Some(caps) = mi1.captures(filename) { 63 | let date = &caps[1]; 64 | let time = &caps[2]; 65 | return Some(format!("{}:{}:{} {}:{}:{}", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4], &time[4..6])); 66 | } 67 | // Generic date-time 68 | if let Some(caps) = dt1.captures(filename) { 69 | let date = format!("{}{}{}", &caps[1], &caps[2], &caps[3]); 70 | let time = &caps[4]; 71 | return Some(format!("{}:{}:{} {}:{}:{}", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4], &time[4..6])); 72 | } 73 | // Custom 74 | if let Some(caps) = custom1.captures(filename) { 75 | let date = &caps[1]; 76 | let time = &caps[2]; 77 | return Some(format!("{}:{}:{} {}:{}:00", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4])); 78 | } 79 | // Try new patterns before fallback 80 | if let Some(caps) = samsung1.captures(filename) { 81 | let date = &caps[1]; 82 | let time = &caps[2]; 83 | return Some(format!("{}:{}:{} {}:{}:{}", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4], &time[4..6])); 84 | } 85 | if let Some(caps) = samsung2.captures(filename) { 86 | return Some(format!("{}:{}:{} {}:{}:{}", &caps[1], &caps[2], &caps[3], &caps[4], &caps[5], &caps[6])); 87 | } 88 | if let Some(caps) = samsung3.captures(filename) { 89 | let date = &caps[1]; 90 | let time = &caps[2]; 91 | return Some(format!("{}:{}:{} {}:{}:{}", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4], &time[4..6])); 92 | } 93 | if let Some(caps) = samsung4.captures(filename) { 94 | return Some(format!("{}:{}:{} {}:{}:{}", &caps[1], &caps[2], &caps[3], &caps[4], &caps[5], &caps[6])); 95 | } 96 | if let Some(caps) = samsung5.captures(filename) { 97 | return Some(format!("{}:{}:{} {}:{}:{}", &caps[1], &caps[2], &caps[3], &caps[4], &caps[5], &caps[6])); 98 | } 99 | if let Some(caps) = samsung6.captures(filename) { 100 | return Some(format!("{}:{}:{} {}:{}:{}", &caps[1], &caps[2], &caps[3], &caps[4], &caps[5], &caps[6])); 101 | } 102 | if let Some(caps) = pxl.captures(filename) { 103 | let date = &caps[1]; 104 | let time = &caps[2][..6]; 105 | return Some(format!("{}:{}:{} {}:{}:{}", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4], &time[4..6])); 106 | } 107 | if let Some(caps) = ms1.captures(filename) { 108 | return Some(format!("{}:{}:{} {}:{}:{}", &caps[1], &caps[2], &caps[3], &caps[4], &caps[5], &caps[6])); 109 | } 110 | if let Some(caps) = ms2.captures(filename) { 111 | return Some(format!("{}:{}:{} {}:{}:{}", &caps[1], &caps[2], &caps[3], &caps[4], &caps[5], &caps[6])); 112 | } 113 | if let Some(caps) = vid.captures(filename) { 114 | let date = &caps[1]; 115 | let time = &caps[2]; 116 | return Some(format!("{}:{}:{} {}:{}:{}", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4], &time[4..6])); 117 | } 118 | if let Some(caps) = sony1.captures(filename) { 119 | let date = &caps[1]; 120 | let time = &caps[2]; 121 | return Some(format!("{}:{}:{} {}:{}:{}", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4], &time[4..6])); 122 | } 123 | if let Some(caps) = sony2.captures(filename) { 124 | let date = &caps[1]; 125 | let time = &caps[2]; 126 | return Some(format!("{}:{}:{} {}:{}:{}", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4], &time[4..6])); 127 | } 128 | if let Some(caps) = rmlmc.captures(filename) { 129 | let date = &caps[1]; 130 | let time = &caps[2]; 131 | return Some(format!("{}:{}:{} {}:{}:{}", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4], &time[4..6])); 132 | } 133 | if let Some(caps) = wallpaper.captures(filename) { 134 | let date = &caps[1]; 135 | let time = &caps[2]; 136 | return Some(format!("{}:{}:{} {}:{}:{}", &date[0..4], &date[4..6], &date[6..8], &time[0..2], &time[2..4], &time[4..6])); 137 | } 138 | if let Some(caps) = san1.captures(filename) { 139 | // e.g. San-1 Oct 2024.jxl 140 | let day = caps[1].parse::().unwrap_or(1); 141 | let month = match &caps[2] { 142 | "Jan" => 1, "Feb" => 2, "Mar" => 3, "Apr" => 4, "May" => 5, "Jun" => 6, 143 | "Jul" => 7, "Aug" => 8, "Sep" => 9, "Oct" => 10, "Nov" => 11, "Dec" => 12, _ => 1 144 | }; 145 | let year = caps[3].parse::().unwrap_or(1970); 146 | return Some(format!("{:04}:{:02}:{:02} 00:00:00", year, month, day)); 147 | } 148 | None 149 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // main.rs 2 | // Entry point for MetaSort_v1.0.0 – Google Photos Takeout Organizer 3 | 4 | mod media_cleaning; 5 | mod metadata_extraction; 6 | mod metadata_embed; 7 | mod sort_to_folders; 8 | mod html_report; 9 | mod utils; 10 | mod csv_report; 11 | mod filename_date_guess; 12 | mod platform; 13 | mod ui; 14 | 15 | use std::io; 16 | use std::path::PathBuf; 17 | use std::fs; 18 | use walkdir; 19 | use fs_extra; 20 | use crate::platform::{is_exiftool_available, get_installation_instructions}; 21 | use crate::ui::MetaSortUI; 22 | 23 | fn get_folder_size(path: &str) -> u64 { 24 | walkdir::WalkDir::new(path) 25 | .into_iter() 26 | .filter_map(Result::ok) 27 | .filter(|e| e.path().is_file()) 28 | .map(|e| e.metadata().map(|m| m.len()).unwrap_or(0)) 29 | .sum() 30 | } 31 | 32 | fn human_readable_size(size: u64) -> String { 33 | const KB: u64 = 1024; 34 | const MB: u64 = KB * 1024; 35 | const GB: u64 = MB * 1024; 36 | match size { 37 | s if s >= GB => format!("{:.2} GB", s as f64 / GB as f64), 38 | s if s >= MB => format!("{:.2} MB", s as f64 / MB as f64), 39 | s if s >= KB => format!("{:.2} KB", s as f64 / KB as f64), 40 | _ => format!("{} B", size), 41 | } 42 | } 43 | 44 | fn main() { 45 | MetaSortUI::print_header(); 46 | 47 | // Check if exiftool is available 48 | if !is_exiftool_available() { 49 | MetaSortUI::print_error("ExifTool is not installed or not found in PATH!"); 50 | println!("{}", get_installation_instructions()); 51 | println!("\nPress Enter to exit..."); 52 | let mut input = String::new(); 53 | io::stdin().read_line(&mut input).expect("Failed to read line"); 54 | return; 55 | } 56 | 57 | MetaSortUI::print_success("ExifTool found and ready!"); 58 | println!("\n📂 Please drag and drop your Google Photos Takeout folder here, or specify the folder path:"); 59 | let mut input = String::new(); 60 | io::stdin().read_line(&mut input).expect("Failed to read line"); 61 | let input_dir = input.trim(); 62 | 63 | // Calculate input folder size and prompt for required space 64 | let folder_size = get_folder_size(input_dir); 65 | let required_space = folder_size * 3; 66 | MetaSortUI::print_info(&format!("Input folder size: {}", human_readable_size(folder_size))); 67 | MetaSortUI::print_info(&format!("Recommended free space: {}", human_readable_size(required_space))); 68 | println!("Continue? (y/n)"); 69 | let mut cont = String::new(); 70 | io::stdin().read_line(&mut cont).expect("Failed to read line"); 71 | if !matches!(cont.trim().to_lowercase().as_str(), "y" | "yes") { 72 | MetaSortUI::print_warning("Aborted by user."); 73 | return; 74 | } 75 | 76 | // Prompt for output folder 77 | println!("\n📁 Please specify the output folder where MetaSort should work (originals will be untouched):"); 78 | let mut output = String::new(); 79 | io::stdin().read_line(&mut output).expect("Failed to read line"); 80 | let output_dir = PathBuf::from(output.trim()); 81 | let temp_dir = output_dir.join("MetaSort_temp"); 82 | 83 | // Copy input folder to MetaSort_temp in output directory 84 | MetaSortUI::print_section_header("Copying Files"); 85 | MetaSortUI::print_info("Copying input folder to working directory..."); 86 | 87 | let mut ui = MetaSortUI::new(); 88 | let total_files = count_files_in_directory(input_dir); 89 | ui.start_main_progress(total_files as u64, "Copying files"); 90 | 91 | let mut copy_options = fs_extra::dir::CopyOptions::new(); 92 | copy_options.copy_inside = true; 93 | fs_extra::dir::copy(input_dir, &temp_dir, ©_options).expect("Failed to copy input folder to output working directory"); 94 | 95 | ui.finish_progress("Copy complete!"); 96 | MetaSortUI::print_success(&format!("All processing will happen in: {}", temp_dir.display())); 97 | 98 | // 1. Clean and pair media files with their JSONs (fix weird JSON names) 99 | MetaSortUI::print_section_header("Cleaning and Pairing Files"); 100 | MetaSortUI::print_info("Cleaning and pairing media files with JSONs..."); 101 | media_cleaning::clean_json_filenames(temp_dir.to_str().unwrap()); 102 | MetaSortUI::print_success("JSON filename cleaning and pairing complete!"); 103 | 104 | // 1b. Ask if WhatsApp/Screenshots should be separated 105 | println!("\nDo you want to separate WhatsApp and Screenshot images? (y/n)"); 106 | let mut wa_sc_input = String::new(); 107 | io::stdin().read_line(&mut wa_sc_input).expect("Failed to read line"); 108 | let separate_wa_sc = matches!(wa_sc_input.trim().to_lowercase().as_str(), "y" | "yes"); 109 | if separate_wa_sc { 110 | MetaSortUI::print_success("WhatsApp and Screenshot images will be sorted into their own folders by year/month."); 111 | } else { 112 | MetaSortUI::print_info("WhatsApp and Screenshot images will be treated as regular photos."); 113 | } 114 | media_cleaning::ask_and_separate_whatsapp_screenshots(temp_dir.to_str().unwrap(), separate_wa_sc); 115 | 116 | // 2. Extract metadata from JSON and embed into media files 117 | MetaSortUI::print_section_header("Metadata Extraction and Embedding"); 118 | MetaSortUI::print_info("Extracting metadata from JSON and embedding into media files..."); 119 | let (metadata, failed_guess_paths) = metadata_extraction::extract_metadata(temp_dir.to_str().unwrap()); 120 | metadata_embed::embed_metadata_all(&metadata, &temp_dir); 121 | MetaSortUI::print_success("Metadata extraction and embedding complete!"); 122 | 123 | // 3. Sort files using the embedded metadata (DateTimeOriginal) 124 | MetaSortUI::print_section_header("Sorting Files"); 125 | MetaSortUI::print_info("Sorting files using embedded metadata..."); 126 | let final_output_dir = output_dir.join("MetaSort_Output"); 127 | sort_to_folders::sort_files_to_folders(&temp_dir, &final_output_dir, &failed_guess_paths, separate_wa_sc); 128 | MetaSortUI::print_success("All done! Check your output and logs for details."); 129 | 130 | // 4. Move technical folders into Technical Files 131 | let technical_dir = final_output_dir.join("Technical Files"); 132 | let _ = fs::create_dir_all(&technical_dir); 133 | for folder in &["CSV Report", "Json Files", "logs"] { 134 | let src = final_output_dir.join(folder); 135 | let dst = technical_dir.join(folder); 136 | if src.exists() { 137 | let _ = fs_extra::dir::move_dir(&src, &dst, &fs_extra::dir::CopyOptions::new()); 138 | } 139 | } 140 | 141 | // 5. HTML summary report 142 | let photos = count_files(&final_output_dir.join("Media Files/Photos")); 143 | let videos = count_files(&final_output_dir.join("Media Files/Videos")); 144 | let whatsapp = count_files(&final_output_dir.join("Media Files/Whatsapp")); 145 | let screenshots = count_files(&final_output_dir.join("Media Files/Screenshots")); 146 | let unknown = count_files(&final_output_dir.join("Media Files/Unknown Time")); 147 | let mkv = count_files(&final_output_dir.join("Media Files/mkv_files")); 148 | let total = photos + videos + whatsapp + screenshots + unknown + mkv; 149 | let errors = count_log_errors(&final_output_dir.join("Technical Files/logs")); 150 | let csv_files = vec!["photos.csv", "videos.csv", "unknown_time.csv", "mkv_files.csv"]; 151 | let log_files = vec!["media_cleaning.log", "metadata_extraction.log", "metadata_embedding.log", "sorting.log"]; 152 | let metadata_fields: Vec<&str> = if let Some(meta) = metadata.first() { 153 | let mut fields = vec!["media_path", "json_path"]; 154 | if meta.exif_date.is_some() { fields.push("exif_date"); } 155 | if meta.gps_latitude.is_some() { fields.push("gps_latitude"); } 156 | if meta.gps_longitude.is_some() { fields.push("gps_longitude"); } 157 | if meta.gps_altitude.is_some() { fields.push("gps_altitude"); } 158 | if meta.camera_make.is_some() { fields.push("camera_make"); } 159 | if meta.camera_model.is_some() { fields.push("camera_model"); } 160 | fields 161 | } else { 162 | vec!["media_path", "json_path", "exif_date", "gps_latitude", "gps_longitude", "gps_altitude", "camera_make", "camera_model"] 163 | }; 164 | html_report::generate_html_report( 165 | &final_output_dir, 166 | total, photos, videos, whatsapp, screenshots, unknown, mkv, errors, 167 | &csv_files, &log_files, &metadata_fields, 168 | ); 169 | 170 | // Print summary 171 | MetaSortUI::print_summary( 172 | photos, videos, whatsapp, screenshots, unknown, mkv, errors, 173 | &final_output_dir.to_string_lossy() 174 | ); 175 | 176 | // Delete MetaSort_temp folder after all processing 177 | if temp_dir.exists() { 178 | match fs_extra::dir::remove(&temp_dir) { 179 | Ok(_) => MetaSortUI::print_info(&format!("Deleted temporary folder: {}", temp_dir.display())), 180 | Err(e) => MetaSortUI::print_warning(&format!("Could not delete temporary folder: {} (Error: {})", temp_dir.display(), e)), 181 | } 182 | } 183 | 184 | MetaSortUI::print_footer(); 185 | } 186 | 187 | fn count_files(dir: &PathBuf) -> usize { 188 | walkdir::WalkDir::new(dir) 189 | .into_iter() 190 | .filter_map(Result::ok) 191 | .filter(|e| e.path().is_file()) 192 | .count() 193 | } 194 | 195 | fn count_files_in_directory(path: &str) -> usize { 196 | walkdir::WalkDir::new(path) 197 | .into_iter() 198 | .filter_map(Result::ok) 199 | .filter(|e| e.path().is_file()) 200 | .count() 201 | } 202 | 203 | fn count_log_errors(logs_dir: &PathBuf) -> usize { 204 | let mut errors = 0; 205 | if let Ok(entries) = fs::read_dir(logs_dir) { 206 | for entry in entries.flatten() { 207 | if let Ok(content) = fs::read_to_string(entry.path()) { 208 | errors += content.matches("❌").count(); 209 | } 210 | } 211 | } 212 | errors 213 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) 2025 - Sanmith S 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/sort_to_folders.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::io; 3 | use std::fs; 4 | use chrono::Datelike; 5 | use crate::csv_report; 6 | use crate::utils::log_to_file; 7 | use std::io::Write; 8 | use serde_json; 9 | use crate::platform::get_exiftool_command; 10 | 11 | /// Main function to organize files into folders by type and date. 12 | pub fn sort_files_to_folders(input_dir: &Path, output_dir: &Path, failed_guess_paths: &Vec, separate_wa_sc: bool) { 13 | let media_extensions = vec![ 14 | // Images 15 | "jpg", "jpeg", "png", "webp", "heic", "heif", "bmp", "tiff", "gif", "avif", "jxl", "jfif", 16 | // Videos 17 | "mp4", "mov", "mkv", "avi", "webm", "3gp", "m4v", "mpg", "mpeg", "mts", "m2ts", "ts", "flv", 18 | "f4v", "wmv", "asf", "rm", "rmvb", "vob", "ogv", "mxf", "dv", "divx", "xvid" 19 | ]; 20 | 21 | // Collect file info for each category 22 | let mut photos_info = Vec::new(); 23 | let mut videos_info = Vec::new(); 24 | let mut unknown_info = Vec::new(); 25 | let mut mkv_info = Vec::new(); 26 | let mut failed_guess_info = Vec::new(); 27 | 28 | let logs_dir = output_dir.join("Technical Files").join("logs"); 29 | 30 | let all_files: Vec<_> = walkdir::WalkDir::new(input_dir).into_iter().filter_map(Result::ok).filter(|e| e.path().is_file()).collect(); 31 | // Only count media files for progress 32 | let all_media_files: Vec<_> = all_files.iter().filter(|entry| { 33 | let path = entry.path(); 34 | if path.is_file() { 35 | let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase(); 36 | media_extensions.contains(&ext.as_str()) 37 | } else { 38 | false 39 | } 40 | }).collect(); 41 | let total = all_media_files.len(); 42 | let mut processed = 0; 43 | for entry in all_media_files { 44 | let path = entry.path(); 45 | if path.is_file() { 46 | let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase(); 47 | // Use exiftool to get DateTimeOriginal, MIMEType, ImageSize 48 | let output = get_exiftool_command() 49 | .arg("-DateTimeOriginal") 50 | .arg("-MIMEType") 51 | .arg("-ImageSize") 52 | .arg("-FileType") 53 | .arg(path) 54 | .output(); 55 | let mut date_str = String::new(); 56 | let mut mime_type = String::new(); 57 | let mut image_size = String::new(); 58 | let mut file_type = String::new(); 59 | if let Ok(out) = output { 60 | let stdout = String::from_utf8_lossy(&out.stdout); 61 | for line in stdout.lines() { 62 | if line.contains("Date/Time Original") { 63 | date_str = line.split(':').skip(1).collect::>().join(":").trim().to_string(); 64 | } else if line.contains("MIME Type") { 65 | mime_type = line.split(':').skip(1).collect::>().join(":").trim().to_string(); 66 | } else if line.contains("Image Size") { 67 | image_size = line.split(':').skip(1).collect::>().join(":").trim().to_string(); 68 | } else if line.contains("File Type") { 69 | file_type = line.split(':').skip(1).collect::>().join(":").trim().to_string(); 70 | } 71 | } 72 | } 73 | // If date_str is still empty, try to extract from JSON 74 | if date_str.is_empty() { 75 | let json_path = path.with_extension(format!("{}json", ext)); 76 | if json_path.exists() { 77 | if let Ok(json_str) = std::fs::read_to_string(&json_path) { 78 | if let Ok(json_val) = serde_json::from_str::(&json_str) { 79 | if let Some(ts) = json_val["photoTakenTime"]["timestamp"].as_str() { 80 | if let Ok(timestamp) = ts.parse::() { 81 | use chrono::{TimeZone, Utc}; 82 | let dt = Utc.timestamp_opt(timestamp, 0).unwrap(); 83 | date_str = dt.format("%Y:%m:%d %H:%M:%S").to_string(); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | let file_size = path.metadata().map(|m| m.len()).unwrap_or(0); 91 | let filename = path.file_name().unwrap().to_string_lossy().to_string(); 92 | let mut dest_folder = output_dir.join("Media Files"); 93 | // WhatsApp/Screenshot detection 94 | let fname_lc = filename.to_lowercase(); 95 | let is_wa = fname_lc.contains("wa") || fname_lc.contains("whatsapp"); 96 | let is_sc = fname_lc.contains("screenshot"); 97 | if separate_wa_sc && is_wa { 98 | dest_folder.push("Whatsapp"); 99 | if let Some(dt) = parse_exif_date(&date_str) { 100 | dest_folder.push(format!("{}", dt.year())); 101 | dest_folder.push(format!("{}", month_name(dt.month()))); 102 | } 103 | photos_info.push((filename.clone(), file_type.clone(), date_str.clone(), image_size.clone(), human_readable_size(file_size), file_size)); 104 | } else if separate_wa_sc && is_sc { 105 | dest_folder.push("Screenshots"); 106 | if let Some(dt) = parse_exif_date(&date_str) { 107 | dest_folder.push(format!("{}", dt.year())); 108 | dest_folder.push(format!("{}", month_name(dt.month()))); 109 | } 110 | photos_info.push((filename.clone(), file_type.clone(), date_str.clone(), image_size.clone(), human_readable_size(file_size), file_size)); 111 | } else if ext == "mkv" { 112 | let _file_category = "mkv_files".to_string(); 113 | dest_folder.push("mkv_files"); 114 | mkv_info.push((filename.clone(), file_type.clone(), date_str.clone(), image_size.clone(), human_readable_size(file_size), file_size)); 115 | } else if date_str.is_empty() { 116 | if failed_guess_paths.contains(&path.to_path_buf()) { 117 | let _file_category = "Failed Filename Guess".to_string(); 118 | dest_folder.push("Unknown Time"); 119 | dest_folder.push("Failed Filename Guess"); 120 | failed_guess_info.push((filename.clone(), file_type.clone(), date_str.clone(), image_size.clone(), human_readable_size(file_size), file_size)); 121 | } else { 122 | let _file_category = "Unknown Time".to_string(); 123 | dest_folder.push("Unknown Time"); 124 | unknown_info.push((filename.clone(), file_type.clone(), date_str.clone(), image_size.clone(), human_readable_size(file_size), file_size)); 125 | } 126 | } else if mime_type.starts_with("video") || ["mp4","mov","avi","webm","3gp","m4v","mpg","mpeg","mts","m2ts","ts","flv","f4v","wmv","asf","rm","rmvb","vob","ogv","mxf","dv","divx","xvid"].contains(&ext.as_str()) { 127 | // Video 128 | let _file_category = "Videos".to_string(); 129 | dest_folder.push("Videos"); 130 | if let Some(dt) = parse_exif_date(&date_str) { 131 | dest_folder.push(format!("{}", dt.year())); 132 | dest_folder.push(format!("{}", month_name(dt.month()))); 133 | } 134 | videos_info.push((filename.clone(), file_type.clone(), date_str.clone(), image_size.clone(), human_readable_size(file_size), file_size)); 135 | } else { 136 | // Photo 137 | let _file_category = "Photos".to_string(); 138 | dest_folder.push("Photos"); 139 | if let Some(dt) = parse_exif_date(&date_str) { 140 | dest_folder.push(format!("{}", dt.year())); 141 | dest_folder.push(format!("{}", month_name(dt.month()))); 142 | } 143 | photos_info.push((filename.clone(), file_type.clone(), date_str.clone(), image_size.clone(), human_readable_size(file_size), file_size)); 144 | } 145 | // Create destination folder if needed 146 | let _ = fs::create_dir_all(&dest_folder); 147 | let dest_path = dest_folder.join(&filename); 148 | // Copy file 149 | match fs::copy(path, &dest_path) { 150 | Ok(_) => { 151 | log_to_file(&logs_dir, "sorting.log", &format!("Copied {:?} to {:?}", path.file_name().unwrap_or_default(), dest_path)); 152 | } 153 | Err(e) => { 154 | log_to_file(&logs_dir, "sorting.log", &format!("Failed to copy {:?} to {:?}: {}", path.file_name().unwrap_or_default(), dest_path, e)); 155 | } 156 | } 157 | processed += 1; 158 | print_progress(processed, total); 159 | } 160 | } 161 | // Write CSVs for each category in CSV Report folder 162 | let csv_report_folder = output_dir.join("Technical Files").join("CSV Report"); 163 | let _ = fs::create_dir_all(&csv_report_folder); 164 | csv_report::write_csv_report(&csv_report_folder, &photos_info, "photos.csv"); 165 | csv_report::write_csv_report(&csv_report_folder, &videos_info, "videos.csv"); 166 | csv_report::write_csv_report(&csv_report_folder, &unknown_info, "unknown_time.csv"); 167 | csv_report::write_csv_report(&csv_report_folder, &mkv_info, "mkv_files.csv"); 168 | csv_report::write_csv_report(&csv_report_folder, &failed_guess_info, "failed_filename_guess.csv"); 169 | log_to_file(&logs_dir, "sorting.log", "CSV reports written for Photos, Videos, Unknown Time, and mkv_files."); 170 | println!("\n📦 Sorting complete! Sorted {} files.", processed); 171 | println!("\n📄 CSV files are added in: {}\nPlease keep this folder safe for future use!", csv_report_folder.display()); 172 | 173 | let failed_guess_folder = output_dir.join("Media Files").join("Unknown Time").join("Failed Filename Guess"); 174 | let _ = fs::create_dir_all(&failed_guess_folder); 175 | for path in failed_guess_paths { 176 | if let Some(filename) = path.file_name() { 177 | let dest = failed_guess_folder.join(filename); 178 | let _ = fs::rename(path, &dest); 179 | log_to_file(&logs_dir, "sorting.log", &format!("Moved failed guess file {:?} to {:?}", path, dest)); 180 | } 181 | } 182 | } 183 | 184 | fn parse_exif_date(date_str: &str) -> Option { 185 | chrono::NaiveDateTime::parse_from_str(date_str, "%Y:%m:%d %H:%M:%S").ok() 186 | } 187 | 188 | fn month_name(month: u32) -> &'static str { 189 | match month { 190 | 1 => "January", 191 | 2 => "February", 192 | 3 => "March", 193 | 4 => "April", 194 | 5 => "May", 195 | 6 => "June", 196 | 7 => "July", 197 | 8 => "August", 198 | 9 => "September", 199 | 10 => "October", 200 | 11 => "November", 201 | 12 => "December", 202 | _ => "Unknown", 203 | } 204 | } 205 | 206 | fn print_progress(done: usize, total: usize) { 207 | let percent = if total > 0 { (done * 100) / total } else { 100 }; 208 | let bar = format!("{}{}", "🟨".repeat(percent / 4), "⬜".repeat(25 - percent / 4)); 209 | print!("\r📦 Sorting: [{}] {}% ({} / {})", bar, percent, done, total); 210 | let _ = io::stdout().flush(); 211 | if done == total { 212 | println!(); 213 | } 214 | } 215 | 216 | fn human_readable_size(size: u64) -> String { 217 | const KB: u64 = 1024; 218 | const MB: u64 = KB * 1024; 219 | const GB: u64 = MB * 1024; 220 | match size { 221 | s if s >= GB => format!("{:.2} GB", s as f64 / GB as f64), 222 | s if s >= MB => format!("{:.2} MB", s as f64 / MB as f64), 223 | s if s >= KB => format!("{:.2} KB", s as f64 / KB as f64), 224 | _ => format!("{} B", size), 225 | } 226 | } 227 | 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![macOS](https://img.shields.io/badge/macOS-blue?style=flat-square&logo=apple&logoColor=white) 2 |   3 | ![Windows](https://img.shields.io/badge/Windows-blue?style=flat-square&logo=microsoft&logoColor=white) 4 |   5 | ![Linux](https://img.shields.io/badge/Linux-blue?style=flat-square&logo=linux&logoColor=white) 6 |   7 | 8 | [![Donate via UPI](https://img.shields.io/badge/Donate-UPI-blue?logo=googlepay&style=for-the-badge)](https://upier.vercel.app/pay/sanmith@superyes) 9 | [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue?logo=paypal&style=for-the-badge)](https://www.paypal.com/paypalme/iamsanmith) 10 | [![Donate via Ko-fi](https://img.shields.io/badge/Donate-Ko--fi-blue?logo=kofi&style=for-the-badge)](https://ko-fi.com/L3L01JGIUW) 11 | 12 | 13 |
    14 | MetaSort Logo 15 |

    MetaSort v1.0.0

    16 |

    🚀 Google Photos Takeout Organizer

    17 |

    Transform your messy Google Photos Takeout into beautifully organized media libraries!

    18 |
    19 | 20 | --- 21 | 22 | ## 🎯 What is MetaSort? 23 | 24 | **MetaSort** is your all-in-one solution for organizing Google Photos Takeout exports (or any messy media folder). It's lightning-fast, user-friendly, and works on both macOS and Windows. 25 | 26 | ### ✨ What MetaSort Does: 27 | - 🧹 **Cleans up filenames** and removes .json clutter 28 | - 📅 **Extracts dates** from filenames, JSON metadata, or file timestamps 29 | - 🏷️ **Embeds metadata** (date, camera, GPS) directly into your photos/videos 30 | - 📦 **Sorts everything** into organized folders by year/month/type 31 | - 💬 **Separates WhatsApp & Screenshots** (optional) 32 | - 📊 **Generates beautiful reports** (CSV + HTML) 33 | - 🎨 **Beautiful UI** with progress bars and emoji-rich feedback 34 | 35 | --- 36 | 37 | ## 🚀 Quick Start (5 Minutes) 38 | 39 | ### For Non-Technical Users: 40 | 41 | #### macOS: 42 | 1. **Download MetaSort** from GitHub 43 | 2. **Open Terminal** and run: 44 | ```bash 45 | ./scripts/build_macos.sh 46 | ``` 47 | 3. **Double-click** `Run_MetaSort.command` to start! 48 | 49 | #### Windows: 50 | 1. **Download MetaSort** from GitHub 51 | 2. **Right-click** `scripts/install_windows.bat` → "Run as administrator" 52 | 3. **Follow the prompts** - it will install everything automatically! 53 | 54 | ### For Developers: 55 | ```bash 56 | # Clone and build 57 | git clone https://github.com/iamsanmith/MetaSort.git 58 | cd MetaSort 59 | cargo build --release 60 | cargo run --release 61 | ``` 62 | 63 | --- 64 | 65 | ## 💙 Support & Donations 66 | 67 |
    68 | 69 | 70 | 71 | Donate via UPI 75 | 76 |

    77 | 78 | 79 | 80 | Donate with PayPal 85 | 86 |

    87 | 88 | 89 | 90 | Buy Me a Coffee at ko-fi.com 97 | 98 | 99 |

    100 | 101 | If MetaSort saved you hours, please consider supporting the project! 102 | Every contribution, no matter how small, makes a difference and helps keep MetaSort free and actively maintained. 103 | 104 | 105 |
    106 | 107 | 108 | 109 | --- 110 | 111 | ## 📋 Requirements 112 | 113 | ### System Requirements: 114 | - **macOS 10.13+** or **Windows 10+** 115 | - **4GB RAM** (recommended) 116 | - **500MB free space** for the application 117 | 118 | ### Dependencies: 119 | - **ExifTool** - For metadata extraction and embedding 120 | - **Rust** - For building the application 121 | 122 | > 💡 **Don't worry!** Our installation scripts handle all dependencies automatically. 123 | 124 | --- 125 | 126 | ## 🛠️ Detailed Installation 127 | 128 | ### macOS Installation 129 | 130 | #### Option 1: Automated (Recommended) 131 | ```bash 132 | # Download and extract MetaSort 133 | git clone https://github.com/iamsanmith/MetaSort.git 134 | cd MetaSort 135 | 136 | # Build and create launchers 137 | ./scripts/build_macos.sh 138 | ``` 139 | 140 | #### Option 2: Manual Installation 141 | ```bash 142 | # 1. Install Homebrew (if not installed) 143 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 144 | 145 | # 2. Install Rust 146 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 147 | # Press 1 when prompted, then restart Terminal 148 | 149 | # 3. Install ExifTool 150 | brew install exiftool 151 | 152 | # 4. Build MetaSort 153 | git clone https://github.com/iamsanmith/MetaSort.git 154 | cd MetaSort 155 | cargo build --release 156 | ``` 157 | 158 | ### Windows Installation 159 | 160 | #### Option 1: Automated (Recommended) 161 | ```cmd 162 | # Run PowerShell installer 163 | powershell -ExecutionPolicy Bypass -File scripts/install_windows.ps1 164 | 165 | # Or run batch installer 166 | scripts/install_windows.bat 167 | ``` 168 | 169 | #### Option 2: Manual Installation 170 | ```cmd 171 | # 1. Install Rust from https://rustup.rs/ 172 | 173 | # 2. Install ExifTool 174 | winget install ExifTool.ExifTool 175 | 176 | # 3. Build MetaSort 177 | git clone https://github.com/iamsanmith/MetaSort.git 178 | cd MetaSort 179 | cargo build --release 180 | ``` 181 | 182 | --- 183 | 184 | ## 🎮 How to Use MetaSort 185 | 186 | ### Step 1: Launch MetaSort 187 | - **macOS**: Double-click `Run_MetaSort.command` 188 | - **Windows**: Run `cargo run --release` or use the generated executable 189 | 190 | ### Step 2: Select Your Folder 191 | - **Drag and drop** your Google Photos Takeout folder 192 | - Or **type the path** to your media folder 193 | - MetaSort works with any folder containing photos/videos! 194 | 195 | ### Step 3: Choose Options 196 | - **Separate WhatsApp/Screenshots?** (Recommended: Yes) 197 | - **Metadata embedding method** (Recommended: Auto-detect) 198 | - **Output directory** (Default: `MetaSort_Output`) 199 | 200 | ### Step 4: Watch the Magic! ✨ 201 | MetaSort will: 202 | 1. 🔍 Scan your files 203 | 2. 🧹 Clean up filenames 204 | 3. 📅 Extract dates 205 | 4. 🏷️ Embed metadata 206 | 5. 📦 Sort into folders 207 | 6. 📊 Generate reports 208 | 209 | ### Step 5: Enjoy Your Organized Media! 🎉 210 | - **Photos/Videos**: `MetaSort_Output/Media Files/` 211 | - **Reports**: `MetaSort_Output/Technical Files/` 212 | - **HTML Summary**: Open `MetaSort_Output/Technical Files/report.html` 213 | 214 | --- 215 | 216 | ## 📁 Output Structure 217 | 218 | After processing, you'll find: 219 | 220 | ``` 221 | MetaSort_Output/ 222 | ├── Media Files/ 223 | │ ├── 2023/ 224 | │ │ ├── 01_January/ 225 | │ │ │ ├── Photos/ 226 | │ │ │ ├── Videos/ 227 | │ │ │ └── Screenshots/ 228 | │ │ └── 02_February/ 229 | │ └── 2024/ 230 | ├── Technical Files/ 231 | │ ├── report.html # Beautiful summary report 232 | │ ├── processing_log.csv # Detailed processing log 233 | │ ├── metadata_summary.csv # Metadata statistics 234 | │ └── error_log.txt # Any issues encountered 235 | └── Original Files/ # Backup of original structure 236 | ``` 237 | 238 | --- 239 | 240 | ## 🎯 Supported File Types 241 | 242 | ### Media Files: 243 | - **Photos**: JPG, JPEG, PNG, WEBP, HEIC, HEIF, BMP, TIFF, GIF, AVIF, JXL, JFIF 244 | - **Raw Formats**: RAW, CR2, NEF, ORF, SR2, ARW, DNG, PEF, RAF, RW2, SRW, 3FR, ERF, K25, KDC, MEF, MOS, MRW, NRW, SRF, X3F 245 | - **Design Files**: SVG, ICO, PSD, AI, EPS 246 | - **Videos**: MP4, MOV, MKV, AVI, WEBM, 3GP, M4V, MPG, MPEG, MTS, M2TS, TS, FLV, F4V, WMV, ASF, RM, RMVB, VOB, OGV, MXF, DV, DIVX, XVID 247 | 248 | ### Metadata Sources: 249 | - **JSON files** (Google Photos metadata) 250 | - **Filename patterns** (WhatsApp, Screenshots, etc.) 251 | - **EXIF data** (embedded in files) 252 | - **File timestamps** (fallback) 253 | 254 | --- 255 | 256 | ## 📅 Smart Date Detection 257 | 258 | MetaSort can extract dates from countless filename patterns: 259 | 260 | ### 📱 Mobile Apps: 261 | - **WhatsApp**: `IMG-20220101-WA0001.jpg` → `2022:01:01 00:00:00` 262 | - **Screenshots**: `Screenshot_2023-01-01-12-00-00.png` → `2023:01:01 12:00:00` 263 | - **Telegram**: `photo_2023-01-01 12.00.00.jpg` → `2023:01:01 12:00:00` 264 | 265 | ### 📷 Cameras & Phones: 266 | - **Samsung/Android**: `20230101_123456.jpg` → `2023:01:01 12:34:56` 267 | - **Google Photos**: `PXL_20230101_123456789.jpg` → `2023:01:01 12:34:56` 268 | - **Sony Camera**: `DSC01234_20230101_123456.JPG` → `2023:01:01 12:34:56` 269 | - **MIUI**: `IMG_20230101_120000.jpg` → `2023:01:01 12:00:00` 270 | 271 | ### 🎯 Custom Patterns: 272 | - `wallpaper - IMG_20240113_143213Jan 13 2024` → `2024:01:13 14:32:13` 273 | - `San-1 Oct 2024.jxl` → `2024:10:01 00:00:00` 274 | - `RMLmc20250531_115820_RMlmc.7` → `2025:05:31 11:58:20` 275 | 276 | > 💡 **MetaSort is smart!** If your filename contains a date, it will likely find it! 277 | 278 | --- 279 | 280 | ## 🛠️ Advanced Features 281 | 282 | ### 🔧 Command Line Options 283 | ```bash 284 | # Run with specific options 285 | cargo run --release -- --help 286 | 287 | # Process specific folder 288 | cargo run --release -- --input "/path/to/folder" 289 | 290 | # Custom output directory 291 | cargo run --release -- --output "/path/to/output" 292 | ``` 293 | 294 | ### 📊 Report Customization 295 | - **HTML Report**: Beautiful web-based summary with statistics 296 | - **CSV Reports**: Detailed logs for spreadsheet analysis 297 | - **Error Logs**: Track any issues during processing 298 | 299 | ### 🔄 Batch Processing 300 | - Process multiple folders 301 | - Resume interrupted operations 302 | - Skip already processed files 303 | 304 | --- 305 | 306 | ## 🆘 Troubleshooting 307 | 308 | ### Common Issues: 309 | 310 | #### "ExifTool not found" 311 | **macOS:** 312 | ```bash 313 | brew install exiftool 314 | ``` 315 | 316 | **Windows:** 317 | ```cmd 318 | winget install ExifTool.ExifTool 319 | ``` 320 | 321 | #### "Permission denied" 322 | **macOS:** 323 | ```bash 324 | chmod +x scripts/build_macos.sh 325 | ``` 326 | 327 | **Windows:** 328 | - Right-click script → "Run as administrator" 329 | 330 | #### "Rust not found" 331 | **macOS:** 332 | ```bash 333 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 334 | ``` 335 | 336 | **Windows:** 337 | - Download from https://rustup.rs/ 338 | 339 | #### "App won't open" (macOS) 340 | - Use `Run_MetaSort.command` instead of app bundles 341 | - Right-click → Open (if needed) 342 | 343 | ### Getting Help: 344 | 1. Check the **error logs** in `MetaSort_Output/Technical Files/` 345 | 2. Ensure **ExifTool is installed** and accessible 346 | 3. Try **running in terminal** for detailed error messages 347 | 4. **Open an issue** on GitHub with error details 348 | 349 | --- 350 | 351 | ## 🏗️ Project Structure 352 | 353 | ``` 354 | MetaSort/ 355 | ├── 📁 src/ # Source code 356 | │ ├── main.rs # Main application 357 | │ ├── platform.rs # Cross-platform compatibility 358 | │ ├── ui.rs # User interface & progress bars 359 | │ ├── media_cleaning.rs # File cleaning & organization 360 | │ ├── metadata_extraction.rs # JSON metadata extraction 361 | │ ├── metadata_embed.rs # Metadata embedding 362 | │ ├── sort_to_folders.rs # File sorting & folder creation 363 | │ ├── csv_report.rs # CSV report generation 364 | │ ├── html_report.rs # HTML report generation 365 | │ ├── filename_date_guess.rs # Date extraction from filenames 366 | │ └── utils.rs # Utility functions 367 | ├── 📁 scripts/ # Build & installation scripts 368 | │ ├── build_macos.sh # macOS build script 369 | │ ├── build_windows.bat # Windows build script 370 | │ ├── install_windows.ps1 # Windows installer (PowerShell) 371 | │ └── install_windows.bat # Windows installer (Batch) 372 | ├── 📁 docs/ # Documentation 373 | │ ├── SIMPLE_INSTALL.md # Non-technical user guide 374 | │ └── CROSS_PLATFORM_CHANGES.md # Technical details 375 | ├── 📁 assets/ # Resources 376 | │ ├── logo.png # MetaSort logo 377 | │ └── upi.png # UPI QR code 378 | ├── 🚀 Run_MetaSort.command # Easy launcher (macOS) 379 | ├── 🚀 MetaSort.command # Advanced launcher (macOS) 380 | ├── 📄 README.md # This file 381 | ├── 📄 LICENSE.txt # Apache 2.0 License 382 | └── 📄 Cargo.toml # Rust project configuration 383 | ``` 384 | 385 | --- 386 | 387 | ## 🤝 Contributing 388 | 389 | We welcome contributions! Here's how you can help: 390 | 391 | ### 🐛 Report Bugs 392 | 1. Check existing issues first 393 | 2. Provide detailed error messages 394 | 3. Include your OS and MetaSort version 395 | 396 | ### 💡 Suggest Features 397 | 1. Describe the feature clearly 398 | 2. Explain why it would be useful 399 | 3. Consider implementation complexity 400 | 401 | ### 🔧 Submit Code 402 | 1. Fork the repository 403 | 2. Create a feature branch 404 | 3. Make your changes 405 | 4. Test thoroughly 406 | 5. Submit a pull request 407 | 408 | ### 📝 Documentation 409 | - Improve README sections 410 | - Add examples 411 | - Fix typos or unclear instructions 412 | 413 | --- 414 | 415 | ## 📄 License 416 | 417 | MetaSort is licensed under the **Apache License 2.0** - see the [LICENSE.txt](LICENSE.txt) file for details. 418 | 419 | This means you can: 420 | - ✅ Use MetaSort for personal or commercial projects 421 | - ✅ Modify and distribute MetaSort 422 | - ✅ Use MetaSort in proprietary software 423 | - ✅ Distribute modified versions 424 | 425 | **Requirements:** 426 | - Include the original license and copyright notice 427 | - State any changes you made 428 | 429 | --- 430 | 431 | ## 🏆 Acknowledgments 432 | 433 | - **ExifTool** - For powerful metadata handling 434 | - **Rust Community** - For the amazing ecosystem 435 | - **All Contributors** - For making MetaSort better 436 | - **You** - For using and supporting MetaSort! 437 | 438 | --- 439 | 440 |
    441 |

    🎉 Ready to organize your photos?

    442 |

    Get started with MetaSort today!

    443 | 444 | 🚀 Quick Start Guide 445 | 446 |
    447 | 448 | --- 449 | 450 |
    451 | 452 | Made with ❤️ by Sanmith S 453 |
    454 | Transform your digital memories into organized treasures! 455 |
    456 |
    457 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "MetaSort" 7 | version = "1.0.0" 8 | dependencies = [ 9 | "chrono", 10 | "csv", 11 | "fs_extra", 12 | "indicatif", 13 | "regex", 14 | "serde", 15 | "serde_json", 16 | "url", 17 | "walkdir", 18 | ] 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "android-tzdata" 31 | version = "0.1.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 34 | 35 | [[package]] 36 | name = "android_system_properties" 37 | version = "0.1.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 40 | dependencies = [ 41 | "libc", 42 | ] 43 | 44 | [[package]] 45 | name = "autocfg" 46 | version = "1.5.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 49 | 50 | [[package]] 51 | name = "bumpalo" 52 | version = "3.19.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 55 | 56 | [[package]] 57 | name = "cc" 58 | version = "1.2.27" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" 61 | dependencies = [ 62 | "shlex", 63 | ] 64 | 65 | [[package]] 66 | name = "cfg-if" 67 | version = "1.0.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 70 | 71 | [[package]] 72 | name = "chrono" 73 | version = "0.4.41" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 76 | dependencies = [ 77 | "android-tzdata", 78 | "iana-time-zone", 79 | "js-sys", 80 | "num-traits", 81 | "wasm-bindgen", 82 | "windows-link", 83 | ] 84 | 85 | [[package]] 86 | name = "console" 87 | version = "0.16.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" 90 | dependencies = [ 91 | "encode_unicode", 92 | "libc", 93 | "once_cell", 94 | "unicode-width", 95 | "windows-sys 0.60.2", 96 | ] 97 | 98 | [[package]] 99 | name = "core-foundation-sys" 100 | version = "0.8.7" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 103 | 104 | [[package]] 105 | name = "csv" 106 | version = "1.3.1" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" 109 | dependencies = [ 110 | "csv-core", 111 | "itoa", 112 | "ryu", 113 | "serde", 114 | ] 115 | 116 | [[package]] 117 | name = "csv-core" 118 | version = "0.1.12" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" 121 | dependencies = [ 122 | "memchr", 123 | ] 124 | 125 | [[package]] 126 | name = "displaydoc" 127 | version = "0.2.5" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 130 | dependencies = [ 131 | "proc-macro2", 132 | "quote", 133 | "syn", 134 | ] 135 | 136 | [[package]] 137 | name = "encode_unicode" 138 | version = "1.0.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 141 | 142 | [[package]] 143 | name = "form_urlencoded" 144 | version = "1.2.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 147 | dependencies = [ 148 | "percent-encoding", 149 | ] 150 | 151 | [[package]] 152 | name = "fs_extra" 153 | version = "1.3.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 156 | 157 | [[package]] 158 | name = "iana-time-zone" 159 | version = "0.1.63" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 162 | dependencies = [ 163 | "android_system_properties", 164 | "core-foundation-sys", 165 | "iana-time-zone-haiku", 166 | "js-sys", 167 | "log", 168 | "wasm-bindgen", 169 | "windows-core", 170 | ] 171 | 172 | [[package]] 173 | name = "iana-time-zone-haiku" 174 | version = "0.1.2" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 177 | dependencies = [ 178 | "cc", 179 | ] 180 | 181 | [[package]] 182 | name = "icu_collections" 183 | version = "2.0.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 186 | dependencies = [ 187 | "displaydoc", 188 | "potential_utf", 189 | "yoke", 190 | "zerofrom", 191 | "zerovec", 192 | ] 193 | 194 | [[package]] 195 | name = "icu_locale_core" 196 | version = "2.0.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 199 | dependencies = [ 200 | "displaydoc", 201 | "litemap", 202 | "tinystr", 203 | "writeable", 204 | "zerovec", 205 | ] 206 | 207 | [[package]] 208 | name = "icu_normalizer" 209 | version = "2.0.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 212 | dependencies = [ 213 | "displaydoc", 214 | "icu_collections", 215 | "icu_normalizer_data", 216 | "icu_properties", 217 | "icu_provider", 218 | "smallvec", 219 | "zerovec", 220 | ] 221 | 222 | [[package]] 223 | name = "icu_normalizer_data" 224 | version = "2.0.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 227 | 228 | [[package]] 229 | name = "icu_properties" 230 | version = "2.0.1" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 233 | dependencies = [ 234 | "displaydoc", 235 | "icu_collections", 236 | "icu_locale_core", 237 | "icu_properties_data", 238 | "icu_provider", 239 | "potential_utf", 240 | "zerotrie", 241 | "zerovec", 242 | ] 243 | 244 | [[package]] 245 | name = "icu_properties_data" 246 | version = "2.0.1" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 249 | 250 | [[package]] 251 | name = "icu_provider" 252 | version = "2.0.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 255 | dependencies = [ 256 | "displaydoc", 257 | "icu_locale_core", 258 | "stable_deref_trait", 259 | "tinystr", 260 | "writeable", 261 | "yoke", 262 | "zerofrom", 263 | "zerotrie", 264 | "zerovec", 265 | ] 266 | 267 | [[package]] 268 | name = "idna" 269 | version = "1.0.3" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 272 | dependencies = [ 273 | "idna_adapter", 274 | "smallvec", 275 | "utf8_iter", 276 | ] 277 | 278 | [[package]] 279 | name = "idna_adapter" 280 | version = "1.2.1" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 283 | dependencies = [ 284 | "icu_normalizer", 285 | "icu_properties", 286 | ] 287 | 288 | [[package]] 289 | name = "indicatif" 290 | version = "0.17.12" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "4adb2ee6ad319a912210a36e56e3623555817bcc877a7e6e8802d1d69c4d8056" 293 | dependencies = [ 294 | "console", 295 | "portable-atomic", 296 | "unicode-width", 297 | "unit-prefix", 298 | "web-time", 299 | ] 300 | 301 | [[package]] 302 | name = "itoa" 303 | version = "1.0.15" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 306 | 307 | [[package]] 308 | name = "js-sys" 309 | version = "0.3.77" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 312 | dependencies = [ 313 | "once_cell", 314 | "wasm-bindgen", 315 | ] 316 | 317 | [[package]] 318 | name = "libc" 319 | version = "0.2.174" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 322 | 323 | [[package]] 324 | name = "litemap" 325 | version = "0.8.0" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 328 | 329 | [[package]] 330 | name = "log" 331 | version = "0.4.27" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 334 | 335 | [[package]] 336 | name = "memchr" 337 | version = "2.7.5" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 340 | 341 | [[package]] 342 | name = "num-traits" 343 | version = "0.2.19" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 346 | dependencies = [ 347 | "autocfg", 348 | ] 349 | 350 | [[package]] 351 | name = "once_cell" 352 | version = "1.21.3" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 355 | 356 | [[package]] 357 | name = "percent-encoding" 358 | version = "2.3.1" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 361 | 362 | [[package]] 363 | name = "portable-atomic" 364 | version = "1.11.1" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 367 | 368 | [[package]] 369 | name = "potential_utf" 370 | version = "0.1.2" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 373 | dependencies = [ 374 | "zerovec", 375 | ] 376 | 377 | [[package]] 378 | name = "proc-macro2" 379 | version = "1.0.95" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 382 | dependencies = [ 383 | "unicode-ident", 384 | ] 385 | 386 | [[package]] 387 | name = "quote" 388 | version = "1.0.40" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 391 | dependencies = [ 392 | "proc-macro2", 393 | ] 394 | 395 | [[package]] 396 | name = "regex" 397 | version = "1.11.1" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 400 | dependencies = [ 401 | "aho-corasick", 402 | "memchr", 403 | "regex-automata", 404 | "regex-syntax", 405 | ] 406 | 407 | [[package]] 408 | name = "regex-automata" 409 | version = "0.4.9" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 412 | dependencies = [ 413 | "aho-corasick", 414 | "memchr", 415 | "regex-syntax", 416 | ] 417 | 418 | [[package]] 419 | name = "regex-syntax" 420 | version = "0.8.5" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 423 | 424 | [[package]] 425 | name = "rustversion" 426 | version = "1.0.21" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 429 | 430 | [[package]] 431 | name = "ryu" 432 | version = "1.0.20" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 435 | 436 | [[package]] 437 | name = "same-file" 438 | version = "1.0.6" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 441 | dependencies = [ 442 | "winapi-util", 443 | ] 444 | 445 | [[package]] 446 | name = "serde" 447 | version = "1.0.219" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 450 | dependencies = [ 451 | "serde_derive", 452 | ] 453 | 454 | [[package]] 455 | name = "serde_derive" 456 | version = "1.0.219" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 459 | dependencies = [ 460 | "proc-macro2", 461 | "quote", 462 | "syn", 463 | ] 464 | 465 | [[package]] 466 | name = "serde_json" 467 | version = "1.0.140" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 470 | dependencies = [ 471 | "itoa", 472 | "memchr", 473 | "ryu", 474 | "serde", 475 | ] 476 | 477 | [[package]] 478 | name = "shlex" 479 | version = "1.3.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 482 | 483 | [[package]] 484 | name = "smallvec" 485 | version = "1.15.1" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 488 | 489 | [[package]] 490 | name = "stable_deref_trait" 491 | version = "1.2.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 494 | 495 | [[package]] 496 | name = "syn" 497 | version = "2.0.104" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 500 | dependencies = [ 501 | "proc-macro2", 502 | "quote", 503 | "unicode-ident", 504 | ] 505 | 506 | [[package]] 507 | name = "synstructure" 508 | version = "0.13.2" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 511 | dependencies = [ 512 | "proc-macro2", 513 | "quote", 514 | "syn", 515 | ] 516 | 517 | [[package]] 518 | name = "tinystr" 519 | version = "0.8.1" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 522 | dependencies = [ 523 | "displaydoc", 524 | "zerovec", 525 | ] 526 | 527 | [[package]] 528 | name = "unicode-ident" 529 | version = "1.0.18" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 532 | 533 | [[package]] 534 | name = "unicode-width" 535 | version = "0.2.1" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" 538 | 539 | [[package]] 540 | name = "unit-prefix" 541 | version = "0.5.1" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" 544 | 545 | [[package]] 546 | name = "url" 547 | version = "2.5.4" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 550 | dependencies = [ 551 | "form_urlencoded", 552 | "idna", 553 | "percent-encoding", 554 | ] 555 | 556 | [[package]] 557 | name = "utf8_iter" 558 | version = "1.0.4" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 561 | 562 | [[package]] 563 | name = "walkdir" 564 | version = "2.5.0" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 567 | dependencies = [ 568 | "same-file", 569 | "winapi-util", 570 | ] 571 | 572 | [[package]] 573 | name = "wasm-bindgen" 574 | version = "0.2.100" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 577 | dependencies = [ 578 | "cfg-if", 579 | "once_cell", 580 | "rustversion", 581 | "wasm-bindgen-macro", 582 | ] 583 | 584 | [[package]] 585 | name = "wasm-bindgen-backend" 586 | version = "0.2.100" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 589 | dependencies = [ 590 | "bumpalo", 591 | "log", 592 | "proc-macro2", 593 | "quote", 594 | "syn", 595 | "wasm-bindgen-shared", 596 | ] 597 | 598 | [[package]] 599 | name = "wasm-bindgen-macro" 600 | version = "0.2.100" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 603 | dependencies = [ 604 | "quote", 605 | "wasm-bindgen-macro-support", 606 | ] 607 | 608 | [[package]] 609 | name = "wasm-bindgen-macro-support" 610 | version = "0.2.100" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 613 | dependencies = [ 614 | "proc-macro2", 615 | "quote", 616 | "syn", 617 | "wasm-bindgen-backend", 618 | "wasm-bindgen-shared", 619 | ] 620 | 621 | [[package]] 622 | name = "wasm-bindgen-shared" 623 | version = "0.2.100" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 626 | dependencies = [ 627 | "unicode-ident", 628 | ] 629 | 630 | [[package]] 631 | name = "web-time" 632 | version = "1.1.0" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 635 | dependencies = [ 636 | "js-sys", 637 | "wasm-bindgen", 638 | ] 639 | 640 | [[package]] 641 | name = "winapi-util" 642 | version = "0.1.9" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 645 | dependencies = [ 646 | "windows-sys 0.59.0", 647 | ] 648 | 649 | [[package]] 650 | name = "windows-core" 651 | version = "0.61.2" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 654 | dependencies = [ 655 | "windows-implement", 656 | "windows-interface", 657 | "windows-link", 658 | "windows-result", 659 | "windows-strings", 660 | ] 661 | 662 | [[package]] 663 | name = "windows-implement" 664 | version = "0.60.0" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 667 | dependencies = [ 668 | "proc-macro2", 669 | "quote", 670 | "syn", 671 | ] 672 | 673 | [[package]] 674 | name = "windows-interface" 675 | version = "0.59.1" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 678 | dependencies = [ 679 | "proc-macro2", 680 | "quote", 681 | "syn", 682 | ] 683 | 684 | [[package]] 685 | name = "windows-link" 686 | version = "0.1.3" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 689 | 690 | [[package]] 691 | name = "windows-result" 692 | version = "0.3.4" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 695 | dependencies = [ 696 | "windows-link", 697 | ] 698 | 699 | [[package]] 700 | name = "windows-strings" 701 | version = "0.4.2" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 704 | dependencies = [ 705 | "windows-link", 706 | ] 707 | 708 | [[package]] 709 | name = "windows-sys" 710 | version = "0.59.0" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 713 | dependencies = [ 714 | "windows-targets 0.52.6", 715 | ] 716 | 717 | [[package]] 718 | name = "windows-sys" 719 | version = "0.60.2" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 722 | dependencies = [ 723 | "windows-targets 0.53.2", 724 | ] 725 | 726 | [[package]] 727 | name = "windows-targets" 728 | version = "0.52.6" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 731 | dependencies = [ 732 | "windows_aarch64_gnullvm 0.52.6", 733 | "windows_aarch64_msvc 0.52.6", 734 | "windows_i686_gnu 0.52.6", 735 | "windows_i686_gnullvm 0.52.6", 736 | "windows_i686_msvc 0.52.6", 737 | "windows_x86_64_gnu 0.52.6", 738 | "windows_x86_64_gnullvm 0.52.6", 739 | "windows_x86_64_msvc 0.52.6", 740 | ] 741 | 742 | [[package]] 743 | name = "windows-targets" 744 | version = "0.53.2" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" 747 | dependencies = [ 748 | "windows_aarch64_gnullvm 0.53.0", 749 | "windows_aarch64_msvc 0.53.0", 750 | "windows_i686_gnu 0.53.0", 751 | "windows_i686_gnullvm 0.53.0", 752 | "windows_i686_msvc 0.53.0", 753 | "windows_x86_64_gnu 0.53.0", 754 | "windows_x86_64_gnullvm 0.53.0", 755 | "windows_x86_64_msvc 0.53.0", 756 | ] 757 | 758 | [[package]] 759 | name = "windows_aarch64_gnullvm" 760 | version = "0.52.6" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 763 | 764 | [[package]] 765 | name = "windows_aarch64_gnullvm" 766 | version = "0.53.0" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 769 | 770 | [[package]] 771 | name = "windows_aarch64_msvc" 772 | version = "0.52.6" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 775 | 776 | [[package]] 777 | name = "windows_aarch64_msvc" 778 | version = "0.53.0" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 781 | 782 | [[package]] 783 | name = "windows_i686_gnu" 784 | version = "0.52.6" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 787 | 788 | [[package]] 789 | name = "windows_i686_gnu" 790 | version = "0.53.0" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 793 | 794 | [[package]] 795 | name = "windows_i686_gnullvm" 796 | version = "0.52.6" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 799 | 800 | [[package]] 801 | name = "windows_i686_gnullvm" 802 | version = "0.53.0" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 805 | 806 | [[package]] 807 | name = "windows_i686_msvc" 808 | version = "0.52.6" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 811 | 812 | [[package]] 813 | name = "windows_i686_msvc" 814 | version = "0.53.0" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 817 | 818 | [[package]] 819 | name = "windows_x86_64_gnu" 820 | version = "0.52.6" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 823 | 824 | [[package]] 825 | name = "windows_x86_64_gnu" 826 | version = "0.53.0" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 829 | 830 | [[package]] 831 | name = "windows_x86_64_gnullvm" 832 | version = "0.52.6" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 835 | 836 | [[package]] 837 | name = "windows_x86_64_gnullvm" 838 | version = "0.53.0" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 841 | 842 | [[package]] 843 | name = "windows_x86_64_msvc" 844 | version = "0.52.6" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 847 | 848 | [[package]] 849 | name = "windows_x86_64_msvc" 850 | version = "0.53.0" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 853 | 854 | [[package]] 855 | name = "writeable" 856 | version = "0.6.1" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 859 | 860 | [[package]] 861 | name = "yoke" 862 | version = "0.8.0" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 865 | dependencies = [ 866 | "serde", 867 | "stable_deref_trait", 868 | "yoke-derive", 869 | "zerofrom", 870 | ] 871 | 872 | [[package]] 873 | name = "yoke-derive" 874 | version = "0.8.0" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 877 | dependencies = [ 878 | "proc-macro2", 879 | "quote", 880 | "syn", 881 | "synstructure", 882 | ] 883 | 884 | [[package]] 885 | name = "zerofrom" 886 | version = "0.1.6" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 889 | dependencies = [ 890 | "zerofrom-derive", 891 | ] 892 | 893 | [[package]] 894 | name = "zerofrom-derive" 895 | version = "0.1.6" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 898 | dependencies = [ 899 | "proc-macro2", 900 | "quote", 901 | "syn", 902 | "synstructure", 903 | ] 904 | 905 | [[package]] 906 | name = "zerotrie" 907 | version = "0.2.2" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 910 | dependencies = [ 911 | "displaydoc", 912 | "yoke", 913 | "zerofrom", 914 | ] 915 | 916 | [[package]] 917 | name = "zerovec" 918 | version = "0.11.2" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 921 | dependencies = [ 922 | "yoke", 923 | "zerofrom", 924 | "zerovec-derive", 925 | ] 926 | 927 | [[package]] 928 | name = "zerovec-derive" 929 | version = "0.11.1" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 932 | dependencies = [ 933 | "proc-macro2", 934 | "quote", 935 | "syn", 936 | ] 937 | --------------------------------------------------------------------------------