<<<<<<< HEAD

 
import os
import shutil
import datetime
import asyncio
import aiofiles
from collections import defaultdict
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
import threading
import time
 
class AdvancedFileOrganizer:
    def __init__(self, master):
        self.master = master
        self.master.title("Advanced File Organizer")
        self.master.geometry("800x600")
 
        self.search_dir = tk.StringVar()
        self.output_dir = tk.StringVar()
        self.dry_run = tk.BooleanVar(value=True)
        self.log_text = tk.StringVar()
 
        self.create_widgets()
 
        self.extension_mapping = {
            # Existing mappings
            '.jpg': 'Images', '.jpeg': 'Images', '.png': 'Images', '.gif': 'Images',
            '.bmp': 'Images', '.tiff': 'Images', '.eps': 'Images', '.psd': 'Images',
            '.webp': 'Images', '.svg': 'Images', '.ico': 'Images', '.heic': 'Images', 
            '.raw': 'Images', '.psd': 'Images', '.xcf': 'Images', 
 
            '.lnk': 'Shortcuts',
 
            '.pdf': 'Documents', 
            '.txt': 'Documents', '.md': 'Documents', '.rtf': 'Documents', 
            '.doc': 'Documents', '.docx': 'Documents', '.odt': 'Documents',
 
            '.csv': 'Spreadsheets', '.xlsx': 'Spreadsheets', '.xls': 'Spreadsheets', '.ods': 'Spreadsheets',
 
            '.wav': 'Audio', '.mp3': 'Audio', '.aiff': 'Audio', '.aac': 'Audio',
            '.wma': 'Audio', '.flac': 'Audio', '.mid': 'Audio', '.midi': 'Audio',
            '.m4a': 'Audio', '.ogg': 'Audio', '.m4p': 'Audio', 
 
            '.mp4': 'Videos', '.avi': 'Videos', '.mov': 'Videos', '.wmv': 'Videos',  
            '.flv': 'Videos', '.mkv': 'Videos', '.webm': 'Videos', '.mpg': 'Videos', 
            '.mpeg': 'Videos', '.aep': 'Videos', '.prproj': 'Videos', 
 
            '.stl': '3D Prints', '.ply': '3D Prints', '.3mf': '3D Prints', 
            '.gcode': '3D Prints', '.obj': '3D Prints',
 
            '.ttc': 'Fonts', '.otf': 'Fonts', '.eot': 'Fonts', '.fon': 'Fonts', 
            '.ttf': 'Fonts', '.woff': 'Fonts', '.woff2': 'Fonts',
 
            '.zip': 'Compressed', '.rar': 'Compressed', '.7z': 'Compressed', 
            '.tar': 'Compressed', '.gz': 'Compressed',
 
            '.exe': 'Executables', '.msi': 'Executables', '.app': 'Executables',
 
            '.py': 'Code', '.java': 'Code', '.cpp': 'Code', '.h': 'Code', 
            '.js': 'Code', '.html': 'Code', '.css': 'Code', '.php': 'Code', 
            '.sh': 'Code', '.bat': 'Code',
 
            '.ppt': 'Presentations', '.pptx': 'Presentations', '.key': 'Presentations', '.odp': 'Presentations',
 
            '.epub': 'eBooks', '.mobi': 'eBooks', '.azw': 'eBooks',
 
            '.db': 'Databases', '.sqlite': 'Databases', '.sql': 'Databases',
 
            '.vdi': 'Virtual Machines', '.vmdk': 'Virtual Machines', '.ova': 'Virtual Machines',
 
            '.indd': 'Design', '.qxd': 'Design', '.ai': 'Design', 
            '.cdr': 'Design', '.dwg': 'Design', '.dxf': 'Design', 
 
            '.iso': 'Disk Images', '.dmg': 'Disk Images',
 
            '.json': 'Data Files', '.xml': 'Data Files', '.yaml': 'Data Files',
 
            # New mappings
            '.step': 'Design', '.stp': 'Design', '.iges': 'Design', '.igs': 'Design',
            '.eps': 'Design', '.svg': 'Design',
            '.bak': 'Backups', '.old': 'Backups', '.tmp': 'Backups',
            '.ini': 'ConfigFiles', '.cfg': 'ConfigFiles', '.conf': 'ConfigFiles', '.config': 'ConfigFiles',
            '.ps1': 'Code', '.vbs': 'Code', '.bash': 'Code',
            '.log': 'Logs', '.syslog': 'Logs'
        }
 
    def create_widgets(self):
        # Search directory selection
        ttk.Label(self.master, text="Search Directory:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        ttk.Entry(self.master, textvariable=self.search_dir, width=50).grid(row=0, column=1, padx=5, pady=5)
        ttk.Button(self.master, text="Browse", command=self.browse_search_dir).grid(row=0, column=2, padx=5, pady=5)
 
        # Output directory selection
        ttk.Label(self.master, text="Output Directory:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
        ttk.Entry(self.master, textvariable=self.output_dir, width=50).grid(row=1, column=1, padx=5, pady=5)
        ttk.Button(self.master, text="Browse", command=self.browse_output_dir).grid(row=1, column=2, padx=5, pady=5)
 
        # Dry run checkbox
        ttk.Checkbutton(self.master, text="Dry Run", variable=self.dry_run).grid(row=2, column=0, padx=5, pady=5, sticky="w")
 
        # Run button
        ttk.Button(self.master, text="Run", command=self.run_organizer).grid(row=2, column=1, padx=5, pady=5)
 
        # Log display
        self.log_display = tk.Text(self.master, wrap=tk.WORD, width=80, height=20)
        self.log_display.grid(row=3, column=0, columnspan=3, padx=5, pady=5)
 
        # Scrollbar for log display
        scrollbar = ttk.Scrollbar(self.master, orient="vertical", command=self.log_display.yview)
        scrollbar.grid(row=3, column=3, sticky="ns")
        self.log_display.configure(yscrollcommand=scrollbar.set)
 
        # Commit changes button (initially disabled)
        self.commit_button = ttk.Button(self.master, text="Commit Changes", command=self.run_commit_changes, state="disabled")
        self.commit_button.grid(row=4, column=1, padx=5, pady=5)
 
        # Progress bar
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(self.master, variable=self.progress_var, maximum=100)
        self.progress_bar.grid(row=5, column=0, columnspan=3, padx=5, pady=5, sticky="ew")
 
    def browse_search_dir(self):
        directory = filedialog.askdirectory()
        self.search_dir.set(directory)
 
    def browse_output_dir(self):
        directory = filedialog.askdirectory()
        self.output_dir.set(directory)
 
    def run_organizer(self):
        self.log_display.delete(1.0, tk.END)
        self.commit_button.config(state="disabled")
        
        if not self.search_dir.get() or not self.output_dir.get():
            self.log("Please select both search and output directories.")
            return
 
        # Run the organizer in a separate thread to keep the UI responsive
        threading.Thread(target=self._run_organizer_thread, daemon=True).start()
 
    def _run_organizer_thread(self):
        search_directory = self.search_dir.get()
        output_directory = self.output_dir.get()
        is_dry_run = self.dry_run.get()
 
        date_code = datetime.datetime.now().strftime("%m%d%Y")
        self.log(f"Searching directory: {search_directory}")
        self.log(f"Output directory: {output_directory}")
        self.log(f"Dry run: {'Yes' if is_dry_run else 'No'}")
 
        categorized_files = defaultdict(list)
 
        # Scan the directory and categorize files
        total_files = 0
        for root, _, files in os.walk(search_directory):
            for file in files:
                total_files += 1
                file_path = os.path.join(root, file)
                _, ext = os.path.splitext(file)
                category = self.extension_mapping.get(ext.lower(), 'Misc')
                categorized_files[category].append(file_path)
 
        # Process files
        self.changes = []
        processed_files = 0
        for category, files in categorized_files.items():
            if files:
                target_folder = os.path.join(output_directory, f'{category}_{date_code}')
                self.log(f"Category: {category}")
                for file_path in files:
                    target_path = os.path.join(target_folder, os.path.basename(file_path))
                    self.changes.append((file_path, target_path))
                    # Extract the filename from the file path
                    filename = os.path.basename(file_path)  
                    # Extract the target folder name from the target path
                    target_folder_name = os.path.basename(target_folder)
                    # Format the log message with aligned filenames and target folders
                    self.log(f"  {filename:<30}{target_folder_name}")  
                    processed_files += 1
                    self.update_progress(processed_files, total_files)
 
        if not is_dry_run:
            self.run_commit_changes()
        else:
            self.log("\nDry run completed. Review the log and click 'Commit Changes' to apply.")
            self.master.after(0, lambda: self.commit_button.config(state="normal"))
 
    def update_progress(self, current, total):
        progress = (current / total) * 100
        self.progress_var.set(progress)
        self.master.update_idletasks()
 
    def run_commit_changes(self):
        threading.Thread(target=self._commit_changes_thread, daemon=True).start()
 
    def _commit_changes_thread(self):
        asyncio.run(self.commit_changes())
 
    async def commit_changes(self):
        if not self.changes:
            self.log("No changes to commit.")
            return
 
        total_files = len(self.changes)
        self.progress_var.set(0)
        self.progress_bar["maximum"] = total_files
 
        async def process_batch(batch):
            tasks = [self.async_move_file(source, target) for source, target in batch]
            results = await asyncio.gather(*tasks)
            for result in results:
                self.log(result)
                self.progress_var.set(self.progress_var.get() + 1)
                self.master.update_idletasks()
 
        # Process files in batches of 10
        batch_size = 10
        for i in range(0, len(self.changes), batch_size):
            batch = self.changes[i:i+batch_size]
            await process_batch(batch)
 
        self.log("All changes committed.")
        self.changes = []
        self.commit_button.config(state="disabled")
 
    async def async_move_file(self, source, target):
        try:
            # Create target directory if it doesn't exist
            os.makedirs(os.path.dirname(target), exist_ok=True)
 
            # Check if the file already exists in the target location
            if os.path.exists(target):
                base, extension = os.path.splitext(target)
                counter = 1
                while os.path.exists(f"{base}_{counter}{extension}"):
                    counter += 1
                target = f"{base}_{counter}{extension}"
 
            # Check file size
            file_size = os.path.getsize(source)
            if file_size > 100 * 1024 * 1024:  # 100 MB
                # For large files, use shutil.move with a callback for progress
                await asyncio.to_thread(self.move_large_file, source, target, file_size)
            else:
                # For smaller files, use aiofiles
                async with aiofiles.open(source, 'rb') as src, aiofiles.open(target, 'wb') as dst:
                    await dst.write(await src.read())
                await asyncio.to_thread(os.remove, source)
 
            return f"Moved: '{source}' to '{target}'"
        except PermissionError:
            return f"Permission denied: Unable to move '{source}' to '{target}'"
        except FileNotFoundError:
            return f"File not found: '{source}'"
        except OSError as e:
            if "on a different device" in str(e):
                # File is on a different device (e.g., network drive), use shutil.copy2 and then remove original
                await asyncio.to_thread(shutil.copy2, source, target)
                await asyncio.to_thread(os.remove, source)
                return f"Copied and removed: '{source}' to '{target}' (different device)"
            else:
                return f"Error moving '{source}' to '{target}': {str(e)}"
        except Exception as e:
            return f"Unexpected error moving '{source}' to '{target}': {str(e)}"
 
    def move_large_file(self, source, target, total_size):
        with open(source, 'rb') as src, open(target, 'wb') as dst:
            copied = 0
            while True:
                buf = src.read(8388608)  # 8MB chunks
                if not buf:
                    break
                dst.write(buf)
                copied += len(buf)
                progress = (copied / total_size) * 100
                self.master.after(0, lambda: self.progress_var.set(progress))
        os.remove(source)
 
    def log(self, message):
        self.log_display.insert(tk.END, message + "\n")
        self.log_display.see(tk.END)
 
if __name__ == "__main__":
    root = tk.Tk()
    app = AdvancedFileOrganizer(root)
    root.mainloop()
 
 

=======

obs/v4