Last active
November 19, 2023 04:27
-
-
Save dcrawbuck/02d41bea068c3e732c2d0eab2c637bcd to your computer and use it in GitHub Desktop.
desktop_stacks_organizer.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # A Python script to organizes your desktop files | |
| # by file type, mimicking macOS Stacks | |
| import argparse | |
| import os | |
| import shutil | |
| from tqdm import tqdm | |
| from typing import List | |
| import re | |
| # Mapping of file types to file extensions | |
| FILE_TYPE_MAPPING = { | |
| 'PDFs': ['pdf'], | |
| 'Documents': ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv'], | |
| 'Images': ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'svg', 'psd', 'heic'], | |
| 'Movies': ['mp4', 'mov', 'avi', 'mkv'], | |
| 'Audio': ['wav', 'mp3', 'm4a', 'flac', 'aac', 'ogg', 'wma', 'aiff', 'alac'], | |
| 'Code': ['py', 'java', 'cpp', 'c', 'h', 'swift', 'js', 'html', 'css', 'scss'], | |
| # special cases | |
| # --------------- | |
| # 'Screenshots': png that is named "Screen Shot YYYY-MM-DD..." | |
| # 'Other': all other file extensions | |
| } | |
| # Regex to match default macOS screenshot names | |
| screenshot_regex = r"(Screen Shot|Screenshot) \d{4}-\d{2}-\d{2}.*" | |
| def parse_args(): | |
| parser = argparse.ArgumentParser(description="Organizes files in a specified directory into subdirectories based on file types like PDFs, Documents, Images, etc. similar to macOS Stacks. Can ignore specific file types or extensions.") | |
| parser.add_argument('--input_dir', type=str, help='Input directory path, e.g., "/Users/YourName/Desktop"') | |
| parser.add_argument('--output_dir', type=str, default=None, help='Output directory path, defaults to a subdirectory "organized_files" in the input directory') | |
| parser.add_argument('--ignore_extensions', nargs='+', default=[], help='List of file extensions to ignore, e.g., "txt jpg"') | |
| parser.add_argument('--ignore_types', nargs='+', default=[], help='List of file types to ignore, e.g., "Documents Audio"') | |
| parser.add_argument('--ignore_existing', action='store_true', help='If set, ignores existing directories and creates new ones') | |
| return parser.parse_args() | |
| def organize_files( | |
| input_dir: str, | |
| output_dir: str, | |
| ignore_extensions: List[str], | |
| ignore_types: List[str], | |
| ignore_existing: bool | |
| ) -> None: | |
| # Default output_dir to input_dir if not specified | |
| if output_dir is None: | |
| output_dir = input_dir | |
| # Get all files in the input directory | |
| files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))] | |
| moved_files_count = 0 | |
| unique_directories = set() | |
| created_directories = {} # Track created directories to avoid redundancy | |
| # Iterate through each file | |
| for file in tqdm(files, desc='Organizing files'): | |
| # Ignore hidden files | |
| if file.startswith('.'): | |
| continue | |
| # Get the file extension | |
| file_extension = os.path.splitext(file)[1][1:].lower() | |
| # Ignore specified file extensions | |
| if file_extension in ignore_extensions: | |
| continue | |
| # Get the file type from the mapping | |
| file_type = 'Other' | |
| for key, extensions in FILE_TYPE_MAPPING.items(): | |
| if file_extension in extensions: | |
| file_type = key | |
| break | |
| # Special handling for Screenshots | |
| if file_extension == 'png' and re.match(screenshot_regex, file): | |
| file_type = 'Screenshots' | |
| # Ignore specified file types | |
| if file_type in ignore_types: | |
| continue | |
| # Create a directory for the file type if it doesn't exist | |
| if file_type not in created_directories: | |
| new_dir_name = file_type | |
| base_dir_exists = os.path.exists(os.path.join(output_dir, file_type)) | |
| if not base_dir_exists or (ignore_existing and base_dir_exists): | |
| if not base_dir_exists: | |
| os.makedirs(os.path.join(output_dir, file_type), exist_ok=True) | |
| else: | |
| i = 2 | |
| # Keep incrementing the number until we find a directory name that doesn't exist | |
| while os.path.exists(os.path.join(output_dir, new_dir_name)): | |
| new_dir_name = f"{file_type} {i}" | |
| i += 1 | |
| os.makedirs(os.path.join(output_dir, new_dir_name), exist_ok=True) | |
| created_directories[file_type] = new_dir_name | |
| target_path = os.path.join(output_dir, created_directories[file_type], file) | |
| # Check if the file already exists | |
| if not os.path.exists(target_path): | |
| try: | |
| shutil.move(os.path.join(input_dir, file), target_path) | |
| moved_files_count += 1 | |
| unique_directories.add(file_type) | |
| except PermissionError as e: | |
| tqdm.write(f"Permission error: {e}") | |
| except Exception as e: | |
| tqdm.write(f"Error moving file {file}: {e}") | |
| else: | |
| tqdm.write(f"File already exists, skipping: {target_path}") | |
| print(f"Organizing complete: {moved_files_count} files moved into {len(unique_directories)} unique file type directories.") | |
| def main(): | |
| args = parse_args() | |
| organize_files( | |
| input_dir=args.input_dir, | |
| output_dir=args.output_dir, | |
| ignore_extensions=args.ignore_extensions, | |
| ignore_types=args.ignore_types, | |
| ignore_existing=args.ignore_existing | |
| ) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment