Skip to content

Instantly share code, notes, and snippets.

@dcrawbuck
Last active November 19, 2023 04:27
Show Gist options
  • Select an option

  • Save dcrawbuck/02d41bea068c3e732c2d0eab2c637bcd to your computer and use it in GitHub Desktop.

Select an option

Save dcrawbuck/02d41bea068c3e732c2d0eab2c637bcd to your computer and use it in GitHub Desktop.
desktop_stacks_organizer.py
# 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