Files are now copying after confirmation
This commit is contained in:
+77
-62
@@ -2,67 +2,53 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
def get_exif_date(file_path):
|
def get_exif_date(file_path):
|
||||||
"""
|
"""
|
||||||
Uses exiftool to extract the File Creation Date/Time (CreateDate).
|
Uses exiftool to extract the File Creation Date/Time (CreateDate).
|
||||||
Returns the formatted date string or an error message if extraction fails.
|
Returns the formatted date string or an error message if extraction fails.
|
||||||
"""
|
"""
|
||||||
# Use exiftool to extract the CreateDate tag.
|
|
||||||
# We use -d to define the desired output format: YYYY-MM-DD HH:MM:SS
|
|
||||||
try:
|
try:
|
||||||
# Command: exiftool -d "Format" -CreateDate "file_path"
|
|
||||||
# The -d option allows specifying the desired output format.
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['exiftool', '-d', "%Y-%m-%d", '-CreateDate', file_path],
|
['exiftool', '-d', "%Y-%m-%d", '-CreateDate', file_path],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
check=True
|
check=True
|
||||||
)
|
)
|
||||||
# The result contains "CreateDate : 2026-04-19".
|
|
||||||
# Extract only the value after the colon.
|
|
||||||
output = result.stdout.strip()
|
output = result.stdout.strip()
|
||||||
if ':' in output:
|
if ':' in output:
|
||||||
date_string = output.split(':', 1)[1].strip()
|
date_string = output.split(':', 1)[1].strip()
|
||||||
else:
|
else:
|
||||||
date_string = output
|
date_string = output
|
||||||
if date_string:
|
if date_string:
|
||||||
# Replace '-' with '\' as requested for path compatibility.
|
|
||||||
return date_string.replace('-', '\\')
|
return date_string.replace('-', '\\')
|
||||||
return "Date not found in metadata."
|
return "Date not found in metadata."
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
# This handles cases where exiftool fails (e.g., corrupted file)
|
|
||||||
return f"ERROR: exiftool failed. Check file integrity or permissions. (Details: {e.stderr.strip()})"
|
return f"ERROR: exiftool failed. Check file integrity or permissions. (Details: {e.stderr.strip()})"
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# This handles cases where 'exiftool' command is not found
|
|
||||||
return "CRITICAL ERROR: exiftool command not found. Please ensure exiftool is installed and in your system PATH."
|
return "CRITICAL ERROR: exiftool command not found. Please ensure exiftool is installed and in your system PATH."
|
||||||
|
|
||||||
|
|
||||||
def scan_photos(root_dir, base_target_path):
|
def scan_photos(root_dir, base_target_path):
|
||||||
"""
|
"""
|
||||||
Recursively scans the given directory (root_dir), extracts EXIF date, and prints file details,
|
Recursively scans the given directory (root_dir), extracts EXIF date,
|
||||||
showing the proposed destination path based on the base_target_path.
|
prints file details with proposed destination paths, then asks for confirmation
|
||||||
|
before copying files.
|
||||||
"""
|
"""
|
||||||
# Define accepted extensions (in lowercase)
|
accepted_extensions = ('.jpg', '.jpeg', '.dng', '.raw', '.cr2', '.cr3', '.raf')
|
||||||
accepted_extensions = (
|
|
||||||
'.jpg', '.jpeg', '.dng', '.raw', '.cr2', '.cr3', '.raf'
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"--- Starting photo scan in: {root_dir} ---")
|
print(f"--- Starting photo scan in: {root_dir} ---")
|
||||||
found_files = []
|
found_files = []
|
||||||
|
|
||||||
# 1. Find all files
|
|
||||||
try:
|
try:
|
||||||
for foldername, subfolders, filenames in os.walk(root_dir):
|
for foldername, subfolders, filenames in os.walk(root_dir):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
full_path = os.path.join(foldername, filename)
|
full_path = os.path.join(foldername, filename)
|
||||||
|
|
||||||
# Check the file extension
|
|
||||||
_, ext = os.path.splitext(filename)
|
_, ext = os.path.splitext(filename)
|
||||||
|
|
||||||
# Convert extension to lowercase for case-insensitive matching
|
|
||||||
if ext.lower() in accepted_extensions:
|
if ext.lower() in accepted_extensions:
|
||||||
found_files.append(full_path)
|
found_files.append(full_path)
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print(f"\nFATAL ERROR: The path '{root_dir}' was not found. Please check the drive letter.")
|
print(f"\nFATAL ERROR: The path '{root_dir}' was not found. Please check the drive letter.")
|
||||||
return
|
return
|
||||||
@@ -70,58 +56,87 @@ def scan_photos(root_dir, base_target_path):
|
|||||||
print(f"\nAN UNEXPECTED ERROR OCCURRED DURING DIRECTORY TRAVERSAL: {e}")
|
print(f"\nAN UNEXPECTED ERROR OCCURRED DURING DIRECTORY TRAVERSAL: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if found_files:
|
if not found_files:
|
||||||
print("\n========================================================")
|
|
||||||
print("--- File Name & Proposed Destination Directory ---")
|
|
||||||
print("========================================================\n")
|
|
||||||
|
|
||||||
processed_count = 0
|
|
||||||
for file_path in found_files:
|
|
||||||
# Extract the date using the external tool
|
|
||||||
date_time_str = get_exif_date(file_path)
|
|
||||||
|
|
||||||
# 1. Construct the full destination directory path
|
|
||||||
# The base_target_path is expected to end with a separator.
|
|
||||||
# The date_time_str is in YYYY\MM\DD format (backslash-separated).
|
|
||||||
|
|
||||||
# Explicitly join with backslashes for Windows-style paths.
|
|
||||||
if not base_target_path.endswith('\\'):
|
|
||||||
destination_dir = base_target_path + '\\' + date_time_str
|
|
||||||
else:
|
|
||||||
destination_dir = base_target_path + date_time_str
|
|
||||||
|
|
||||||
print(f"File: {os.path.basename(file_path)}")
|
|
||||||
print(f" -> Extracted Date: {date_time_str}")
|
|
||||||
print(f" -> Proposed Destination Directory: {destination_dir}")
|
|
||||||
print("-" * 40)
|
|
||||||
processed_count += 1
|
|
||||||
|
|
||||||
print(f"\n========================================================")
|
|
||||||
print(f"--- Scan complete. Processed {processed_count} files. ---")
|
|
||||||
print(f"Files with the same date will point to the same destination directory.")
|
|
||||||
else:
|
|
||||||
print("\n--- Scan complete. No specified photo files found in this directory or its subdirectories. ---")
|
print("\n--- Scan complete. No specified photo files found in this directory or its subdirectories. ---")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Collect file info first (file_path, date_str, dest_dir)
|
||||||
|
file_info_list = []
|
||||||
|
for file_path in found_files:
|
||||||
|
date_time_str = get_exif_date(file_path)
|
||||||
|
|
||||||
|
if not base_target_path.endswith('\\'):
|
||||||
|
destination_dir = base_target_path + '\\' + date_time_str
|
||||||
|
else:
|
||||||
|
destination_dir = base_target_path + date_time_str
|
||||||
|
|
||||||
|
file_info_list.append((file_path, date_time_str, destination_dir))
|
||||||
|
|
||||||
|
# Print all items and their proposed destinations
|
||||||
|
print("\n========================================================")
|
||||||
|
print("--- File Name & Proposed Destination Directory ---")
|
||||||
|
print("========================================================\n")
|
||||||
|
|
||||||
|
for file_path, date_time_str, dest_dir in file_info_list:
|
||||||
|
print(f"File: {os.path.basename(file_path)}")
|
||||||
|
print(f" -> Extracted Date: {date_time_str}")
|
||||||
|
print(f" -> Proposed Destination Directory: {dest_dir}")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
print(f"\n========================================================")
|
||||||
|
print(f"--- Scan complete. Found {len(file_info_list)} files. ---")
|
||||||
|
print("Files with the same date will point to the same destination directory.")
|
||||||
|
|
||||||
|
# Ask for confirmation
|
||||||
|
response = input("\nDo you want to copy these items? (y/yes): ").strip().lower()
|
||||||
|
if response not in ('y', 'yes'):
|
||||||
|
print("Aborted. No files were copied.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Copy files
|
||||||
|
copied_count = 0
|
||||||
|
skipped_exists_count = 0
|
||||||
|
created_dirs_count = 0
|
||||||
|
|
||||||
|
for file_path, date_time_str, dest_dir in file_info_list:
|
||||||
|
dest_file = os.path.join(dest_dir, os.path.basename(file_path))
|
||||||
|
|
||||||
|
# Create destination directory if it doesn't exist
|
||||||
|
if not os.path.exists(dest_dir):
|
||||||
|
os.makedirs(dest_dir)
|
||||||
|
created_dirs_count += 1
|
||||||
|
|
||||||
|
# Check if file already exists in destination
|
||||||
|
if os.path.exists(dest_file):
|
||||||
|
print(f"File already exists: {dest_file} (skipped)")
|
||||||
|
skipped_exists_count += 1
|
||||||
|
else:
|
||||||
|
shutil.copy2(file_path, dest_file)
|
||||||
|
copied_count += 1
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n========================================================")
|
||||||
|
print("--- Copy Operation Complete ---")
|
||||||
|
print("========================================================")
|
||||||
|
print(f"Files copied: {copied_count}")
|
||||||
|
if skipped_exists_count > 0:
|
||||||
|
print(f"Files skipped: {skipped_exists_count} (already exists)")
|
||||||
|
if created_dirs_count > 0:
|
||||||
|
print(f"Directories created: {created_dirs_count}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Check if two arguments are provided (script name + drive + path)
|
|
||||||
if len(sys.argv) != 3:
|
if len(sys.argv) != 3:
|
||||||
print("Usage: python photoimport.py <drive_path> <target_display_path>")
|
print("Usage: python photoimport.py <drive_path> <target_display_path>")
|
||||||
print("Example (for Windows): python photoimport.py Q:\\ I:\\Photos")
|
print("Example (for Windows): python photoimport.py Q:\\ I:\\Photos")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Argument 1: The directory to scan (the drive)
|
|
||||||
root_scan_dir = sys.argv[1]
|
root_scan_dir = sys.argv[1]
|
||||||
# Argument 2: The path to display to the user (the base path for grouping)
|
|
||||||
base_target_path = sys.argv[2]
|
base_target_path = sys.argv[2]
|
||||||
|
|
||||||
# Echo the specified path
|
|
||||||
print(f"Using base target path for grouping: {base_target_path}")
|
print(f"Using base target path for grouping: {base_target_path}")
|
||||||
|
|
||||||
# Ensure the scanning path ends with a directory separator
|
|
||||||
# This is important for os.path.join to work correctly
|
|
||||||
if not root_scan_dir.endswith(os.sep):
|
if not root_scan_dir.endswith(os.sep):
|
||||||
root_scan_dir = root_scan_dir + os.sep
|
root_scan_dir = root_scan_dir + os.sep
|
||||||
|
|
||||||
# Perform the scan using the first argument (the drive) and the base path
|
scan_photos(root_scan_dir, base_target_path)
|
||||||
scan_photos(root_scan_dir, base_target_path)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user