From 4ef34f6f88f01d1eef1b0b85ed96a4a1e80a532f Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 21:45:23 +0000 Subject: [PATCH 01/20] Jan 14, 2025, 1:45 PM --- fatx360.py | 263 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 189 insertions(+), 74 deletions(-) diff --git a/fatx360.py b/fatx360.py index f590450..7728e47 100644 --- a/fatx360.py +++ b/fatx360.py @@ -1,16 +1,18 @@ -import tkinter as tk -from tkinter import filedialog, messagebox, ttk import os +import re import shutil import threading -import re +import tkinter as tk +from tkinter import filedialog, messagebox, ttk + def is_fatx_compatible(name): return len(name) <= 42 and all(char.isalnum() or char in "()." for char in name) + def make_fatx_compatible(name): filename, extension = os.path.splitext(name) - filename = re.sub(r'[^\w\s()]', '', filename) + filename = re.sub(r"[^\w\s()]", "", filename) words = filename.split() if words: camel_case_name = words[0].lower() @@ -23,6 +25,7 @@ def make_fatx_compatible(name): camel_case_name = camel_case_name[:max_filename_length] return camel_case_name + extension + class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) @@ -39,20 +42,45 @@ def __init__(self, master=None): def create_widgets(self): self.create_menu() - + dir_frame = ttk.Frame(self) dir_frame.pack(fill=tk.X, padx=10, pady=5) - + self.dir_entry = ttk.Entry(dir_frame) self.dir_entry.pack(side=tk.LEFT, expand=True, fill=tk.X) - - self.select_dir_button = ttk.Button(dir_frame, text="Select Directory", command=self.select_directory) + + self.select_dir_button = ttk.Button( + dir_frame, text="Select Directory", command=self.select_directory + ) self.select_dir_button.pack(side=tk.RIGHT) + # Add radio button frame after dir_frame + self.mode_frame = ttk.LabelFrame(self, text="Operation Mode") + self.mode_frame.pack(fill=tk.X, padx=10, pady=5) + + self.operation_mode = tk.StringVar(value="copy") + self.copy_radio = ttk.Radiobutton( + self.mode_frame, + text="Copy to new directory", + variable=self.operation_mode, + value="copy", + ) + self.copy_radio.pack(side=tk.LEFT, padx=5) + + self.inplace_radio = ttk.Radiobutton( + self.mode_frame, + text="Modify in place", + variable=self.operation_mode, + value="inplace", + ) + self.inplace_radio.pack(side=tk.LEFT, padx=5) + list_frame = ttk.Frame(self) list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) - self.select_all_button = ttk.Button(list_frame, text="Select All", command=self.toggle_select_all) + self.select_all_button = ttk.Button( + list_frame, text="Select All", command=self.toggle_select_all + ) self.select_all_button.pack(side=tk.TOP, anchor=tk.W) self.listbox = tk.Listbox(list_frame, selectmode=tk.MULTIPLE) @@ -62,14 +90,18 @@ def create_widgets(self): options_frame.pack(fill=tk.X, padx=10, pady=5) self.top_level_var = tk.BooleanVar() - self.top_level_check = ttk.Checkbutton(options_frame, text="Rename top-level folders", - variable=self.top_level_var) + self.top_level_check = ttk.Checkbutton( + options_frame, text="Rename top-level folders", variable=self.top_level_var + ) self.top_level_check.pack(side=tk.TOP, anchor=tk.W) self.subfolders_var = tk.BooleanVar() - self.subfolders_check = ttk.Checkbutton(options_frame, text="Rename subfolders", - variable=self.subfolders_var, - command=self.toggle_depth_slider) + self.subfolders_check = ttk.Checkbutton( + options_frame, + text="Rename subfolders", + variable=self.subfolders_var, + command=self.toggle_depth_slider, + ) self.subfolders_check.pack(side=tk.TOP, anchor=tk.W) # Depth slider @@ -78,33 +110,45 @@ def create_widgets(self): self.depth_label = ttk.Label(self.depth_frame, text="Subfolder depth:") self.depth_label.pack(side=tk.LEFT) self.depth_var = tk.IntVar(value=self.max_depth) - self.depth_slider = ttk.Scale(self.depth_frame, from_=1, to=self.max_depth, - orient=tk.HORIZONTAL, variable=self.depth_var, - length=200, command=self.update_depth_label) + self.depth_slider = ttk.Scale( + self.depth_frame, + from_=1, + to=self.max_depth, + orient=tk.HORIZONTAL, + variable=self.depth_var, + length=200, + command=self.update_depth_label, + ) self.depth_slider.pack(side=tk.LEFT, fill=tk.X, expand=True) self.depth_value_label = ttk.Label(self.depth_frame, text=str(self.max_depth)) self.depth_value_label.pack(side=tk.LEFT) self.depth_frame.pack_forget() # Initially hidden self.files_var = tk.BooleanVar() - self.files_check = ttk.Checkbutton(options_frame, text="Rename files", - variable=self.files_var) + self.files_check = ttk.Checkbutton( + options_frame, text="Rename files", variable=self.files_var + ) self.files_check.pack(side=tk.TOP, anchor=tk.W) - self.rename_button = ttk.Button(options_frame, text="Rename Selected", - command=self.rename_selected) + self.rename_button = ttk.Button( + options_frame, text="Rename Selected", command=self.rename_selected + ) self.rename_button.pack(side=tk.TOP, pady=5) progress_frame = ttk.Frame(self) progress_frame.pack(fill=tk.X, padx=10, pady=5) - self.progress = ttk.Progressbar(progress_frame, orient=tk.HORIZONTAL, length=100, mode='determinate') + self.progress = ttk.Progressbar( + progress_frame, orient=tk.HORIZONTAL, length=100, mode="determinate" + ) self.progress.pack(side=tk.LEFT, fill=tk.X, expand=True) self.progress_label = ttk.Label(progress_frame, text="0 / 0") self.progress_label.pack(side=tk.RIGHT) - self.cancel_button = ttk.Button(self, text="Cancel", command=self.cancel_operation, state=tk.DISABLED) + self.cancel_button = ttk.Button( + self, text="Cancel", command=self.cancel_operation, state=tk.DISABLED + ) self.cancel_button.pack(pady=5) def create_menu(self): @@ -130,9 +174,13 @@ def update_listbox(self): self.all_selected = False self.select_all_button.config(text="Select All") except PermissionError: - messagebox.showerror("Permission Error", "Cannot access the selected directory.") + messagebox.showerror( + "Permission Error", "Cannot access the selected directory." + ) except FileNotFoundError: - messagebox.showerror("Directory Not Found", "The selected directory does not exist.") + messagebox.showerror( + "Directory Not Found", "The selected directory does not exist." + ) def toggle_select_all(self): if self.all_selected: @@ -159,20 +207,33 @@ def rename_selected(self): messagebox.showwarning("No Selection", "Please select items to rename.") return - dest_dir = filedialog.askdirectory(title="Select Destination Folder for Renamed Items") - if not dest_dir: - return + # Ask for confirmation if modifying in place + if self.operation_mode.get() == "inplace": + if not messagebox.askyesno( + "Confirm Operation", + "This will modify the files in place. Are you sure you want to continue?", + ): + return + dest_dir = self.directory + else: + dest_dir = filedialog.askdirectory( + title="Select Destination Folder for Renamed Items" + ) + if not dest_dir: + return - self.progress['value'] = 0 - self.rename_button['state'] = 'disabled' - self.cancel_button['state'] = 'normal' + self.progress["value"] = 0 + self.rename_button["state"] = "disabled" + self.cancel_button["state"] = "normal" self.cancel_flag = False self.total_items = self.count_total_items(selected_items) self.processed_items = 0 self.update_progress_label() - thread = threading.Thread(target=self.rename_items_thread, args=(selected_items, dest_dir)) + thread = threading.Thread( + target=self.rename_items_thread, args=(selected_items, dest_dir) + ) thread.start() def count_total_items(self, items): @@ -187,23 +248,39 @@ def count_total_items(self, items): return total def rename_items_thread(self, items, dest_dir): - renamed_dir = os.path.join(dest_dir, "RENAMED") - try: - os.makedirs(renamed_dir, exist_ok=True) - except PermissionError: - self.show_error("Permission Error", "Cannot create the RENAMED directory.") - self.finish_operation() - return + is_copy_mode = self.operation_mode.get() == "copy" + + if is_copy_mode: + renamed_dir = os.path.join(dest_dir, "RENAMED") + try: + os.makedirs(renamed_dir, exist_ok=True) + except PermissionError: + self.show_error( + "Permission Error", "Cannot create the RENAMED directory." + ) + self.finish_operation() + return + else: + renamed_dir = dest_dir for item in items: if self.cancel_flag: - return # Exit the loop if cancelled + return try: full_path = os.path.join(self.directory, item) if os.path.isdir(full_path): - self.process_directory(full_path, renamed_dir, self.top_level_var.get(), self.subfolders_var.get(), self.files_var.get()) + self.process_directory( + full_path, + renamed_dir, + self.top_level_var.get(), + self.subfolders_var.get(), + self.files_var.get(), + is_copy_mode, + ) else: - self.process_file(full_path, renamed_dir, self.files_var.get()) + self.process_file( + full_path, renamed_dir, self.files_var.get(), is_copy_mode + ) except PermissionError: self.show_error("Permission Error", f"Cannot access {item}.") except shutil.Error as e: @@ -212,50 +289,87 @@ def rename_items_thread(self, items, dest_dir): self.show_error("OS Error", f"Error processing {item}: {str(e)}") if not self.cancel_flag: - self.show_success("Rename Complete", "Selected items have been renamed and copied to the RENAMED folder.") + msg = ( + "Selected items have been renamed and copied to the RENAMED folder." + if is_copy_mode + else "Selected items have been renamed in place." + ) + self.show_success("Rename Complete", msg) self.finish_operation() - def process_directory(self, src_dir, dest_parent_dir, rename_top_level, rename_subfolders, rename_files, current_depth=0): - new_dir_name = make_fatx_compatible(os.path.basename(src_dir)) if rename_top_level or (rename_subfolders and current_depth < self.depth_var.get()) else os.path.basename(src_dir) + def process_directory( + self, + src_dir, + dest_parent_dir, + rename_top_level, + rename_subfolders, + rename_files, + is_copy_mode, + current_depth=0, + ): + new_dir_name = ( + make_fatx_compatible(os.path.basename(src_dir)) + if rename_top_level + or (rename_subfolders and current_depth < self.depth_var.get()) + else os.path.basename(src_dir) + ) new_dir_path = os.path.join(dest_parent_dir, new_dir_name) - os.makedirs(new_dir_path, exist_ok=True) + + if is_copy_mode: + os.makedirs(new_dir_path, exist_ok=True) + elif new_dir_name != os.path.basename(src_dir): + os.rename(src_dir, new_dir_path) + src_dir = new_dir_path for root, dirs, files in os.walk(src_dir): if self.cancel_flag: return rel_path = os.path.relpath(root, src_dir) - new_root = os.path.join(new_dir_path, rel_path) - - # Rename subfolders if option is selected and within depth - if rename_subfolders and current_depth < self.depth_var.get() and root != src_dir: - new_root = os.path.join(os.path.dirname(new_root), make_fatx_compatible(os.path.basename(root))) - - os.makedirs(new_root, exist_ok=True) + new_root = os.path.join( + new_dir_path if is_copy_mode else dest_parent_dir, rel_path + ) + + if ( + rename_subfolders + and current_depth < self.depth_var.get() + and root != src_dir + ): + new_name = make_fatx_compatible(os.path.basename(root)) + if new_name != os.path.basename(root): + new_root = os.path.join(os.path.dirname(new_root), new_name) + if not is_copy_mode: + os.rename(root, new_root) + + if is_copy_mode: + os.makedirs(new_root, exist_ok=True) for file in files: if self.cancel_flag: return src_file = os.path.join(root, file) - self.process_file(src_file, new_root, rename_files) + self.process_file(src_file, new_root, rename_files, is_copy_mode) - # Process subdirectories - for dir_name in dirs: - full_dir_path = os.path.join(root, dir_name) - self.process_directory(full_dir_path, new_root, False, rename_subfolders, rename_files, current_depth + 1) + break # Only process top level - # We only want to process the top-level of this directory, so break the loop - break - - def process_file(self, src_file, dest_dir, rename_file): - new_name = make_fatx_compatible(os.path.basename(src_file)) if rename_file else os.path.basename(src_file) + def process_file(self, src_file, dest_dir, rename_file, is_copy_mode): + new_name = ( + make_fatx_compatible(os.path.basename(src_file)) + if rename_file + else os.path.basename(src_file) + ) new_path = os.path.join(dest_dir, new_name) - shutil.copy2(src_file, new_path) + + if is_copy_mode: + shutil.copy2(src_file, new_path) + elif new_name != os.path.basename(src_file): + os.rename(src_file, new_path) + self.processed_items += 1 self.update_progress() def update_progress(self): progress_value = (self.processed_items / self.total_items) * 100 - self.progress['value'] = progress_value + self.progress["value"] = progress_value self.update_progress_label() def update_progress_label(self): @@ -263,38 +377,38 @@ def update_progress_label(self): def cancel_operation(self): self.cancel_flag = True - self.cancel_button['state'] = 'disabled' + self.cancel_button["state"] = "disabled" self.show_info("Operation Cancelled", "The renaming operation was cancelled.") self.reset_interface() def finish_operation(self): - self.rename_button['state'] = 'normal' - self.cancel_button['state'] = 'disabled' + self.rename_button["state"] = "normal" + self.cancel_button["state"] = "disabled" self.cancel_flag = False self.reset_interface() def reset_interface(self): # Reset progress bar - self.progress['value'] = 0 + self.progress["value"] = 0 self.progress_label.config(text="0 / 0") - + # Reset selection self.listbox.selection_clear(0, tk.END) self.all_selected = False self.select_all_button.config(text="Select All") - + # Reset checkboxes self.top_level_var.set(False) self.subfolders_var.set(False) self.files_var.set(False) - + # Hide depth slider self.depth_frame.pack_forget() - + # Reset depth slider value self.depth_var.set(self.max_depth) self.update_depth_label() - + # Reset counters self.total_items = 0 self.processed_items = 0 @@ -308,6 +422,7 @@ def show_success(self, title, message): def show_info(self, title, message): self.master.after(0, lambda: messagebox.showinfo(title, message)) + root = tk.Tk() app = Application(master=root) app.mainloop() From 18185fbb1701700f6864bbe085da2b94726310f4 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 21:49:02 +0000 Subject: [PATCH 02/20] Jan 14, 2025, 1:49 PM --- fatx360.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/fatx360.py b/fatx360.py index 7728e47..0509558 100644 --- a/fatx360.py +++ b/fatx360.py @@ -10,20 +10,23 @@ def is_fatx_compatible(name): return len(name) <= 42 and all(char.isalnum() or char in "()." for char in name) -def make_fatx_compatible(name): +def make_fatx_compatible(name, is_directory=False): filename, extension = os.path.splitext(name) + # Remove invalid characters but keep spaces filename = re.sub(r"[^\w\s()]", "", filename) - words = filename.split() - if words: - camel_case_name = words[0].lower() - for word in words[1:]: - camel_case_name += word.capitalize() + + if is_directory: + # For directories: Keep beginning, truncate from end if needed + max_length = 42 - len(extension) + if len(filename) > max_length: + filename = filename[:max_length] else: - camel_case_name = "" - max_filename_length = 42 - len(extension) - if len(camel_case_name) > max_filename_length: - camel_case_name = camel_case_name[:max_filename_length] - return camel_case_name + extension + # For files: Keep end, truncate from beginning if needed + max_length = 42 - len(extension) + if len(filename) > max_length: + filename = filename[-max_length:] + + return filename + extension class Application(tk.Frame): @@ -308,7 +311,7 @@ def process_directory( current_depth=0, ): new_dir_name = ( - make_fatx_compatible(os.path.basename(src_dir)) + make_fatx_compatible(os.path.basename(src_dir), is_directory=True) if rename_top_level or (rename_subfolders and current_depth < self.depth_var.get()) else os.path.basename(src_dir) @@ -334,7 +337,9 @@ def process_directory( and current_depth < self.depth_var.get() and root != src_dir ): - new_name = make_fatx_compatible(os.path.basename(root)) + new_name = make_fatx_compatible( + os.path.basename(root), is_directory=True + ) if new_name != os.path.basename(root): new_root = os.path.join(os.path.dirname(new_root), new_name) if not is_copy_mode: @@ -353,7 +358,7 @@ def process_directory( def process_file(self, src_file, dest_dir, rename_file, is_copy_mode): new_name = ( - make_fatx_compatible(os.path.basename(src_file)) + make_fatx_compatible(os.path.basename(src_file), is_directory=False) if rename_file else os.path.basename(src_file) ) From 5bab99c8c2256a9ec89258fba9f0a16f10048167 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 21:50:22 +0000 Subject: [PATCH 03/20] Jan 14, 2025, 1:50 PM --- fatx360.py | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/fatx360.py b/fatx360.py index 0509558..b4d80b1 100644 --- a/fatx360.py +++ b/fatx360.py @@ -7,7 +7,19 @@ def is_fatx_compatible(name): - return len(name) <= 42 and all(char.isalnum() or char in "()." for char in name) + """Check if a filename is FATX compatible without changing it.""" + # Check length (including extension) + if len(name) > 42: + return False + + # Check for invalid characters + valid_chars = set( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789()." + ) + if not all(char in valid_chars or char.isspace() for char in name): + return False + + return True def make_fatx_compatible(name, is_directory=False): @@ -310,17 +322,23 @@ def process_directory( is_copy_mode, current_depth=0, ): + # First check if renaming is actually needed + orig_name = os.path.basename(src_dir) new_dir_name = ( - make_fatx_compatible(os.path.basename(src_dir), is_directory=True) + make_fatx_compatible(orig_name, is_directory=True) if rename_top_level or (rename_subfolders and current_depth < self.depth_var.get()) - else os.path.basename(src_dir) + else orig_name ) - new_dir_path = os.path.join(dest_parent_dir, new_dir_name) + + # Only use the new name if it's different and needs to be changed + needs_rename = not is_fatx_compatible(orig_name) + final_name = new_dir_name if needs_rename else orig_name + new_dir_path = os.path.join(dest_parent_dir, final_name) if is_copy_mode: os.makedirs(new_dir_path, exist_ok=True) - elif new_dir_name != os.path.basename(src_dir): + elif needs_rename: # Only rename if necessary os.rename(src_dir, new_dir_path) src_dir = new_dir_path @@ -357,16 +375,21 @@ def process_directory( break # Only process top level def process_file(self, src_file, dest_dir, rename_file, is_copy_mode): + orig_name = os.path.basename(src_file) new_name = ( - make_fatx_compatible(os.path.basename(src_file), is_directory=False) + make_fatx_compatible(orig_name, is_directory=False) if rename_file - else os.path.basename(src_file) + else orig_name ) - new_path = os.path.join(dest_dir, new_name) + + # Only proceed with rename if the name actually needs to change + needs_rename = rename_file and not is_fatx_compatible(orig_name) + final_name = new_name if needs_rename else orig_name + new_path = os.path.join(dest_dir, final_name) if is_copy_mode: shutil.copy2(src_file, new_path) - elif new_name != os.path.basename(src_file): + elif needs_rename: # Only rename if necessary os.rename(src_file, new_path) self.processed_items += 1 From 302d1b64f97e53ec8cf94f1d76f44334a1bcd338 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 21:51:35 +0000 Subject: [PATCH 04/20] Jan 14, 2025, 1:51 PM --- fatx360.py | 193 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 127 insertions(+), 66 deletions(-) diff --git a/fatx360.py b/fatx360.py index b4d80b1..623845b 100644 --- a/fatx360.py +++ b/fatx360.py @@ -1,10 +1,15 @@ +import multiprocessing import os import re import shutil import threading import tkinter as tk +from concurrent.futures import ThreadPoolExecutor from tkinter import filedialog, messagebox, ttk +# Use 75% of available cores (minimum 2) to avoid overwhelming the system +CPU_COUNT = max(2, int(multiprocessing.cpu_count() * 0.75)) + def is_fatx_compatible(name): """Check if a filename is FATX compatible without changing it.""" @@ -278,30 +283,62 @@ def rename_items_thread(self, items, dest_dir): else: renamed_dir = dest_dir + # Create a list of work items + work_items = [] for item in items: - if self.cancel_flag: - return - try: - full_path = os.path.join(self.directory, item) - if os.path.isdir(full_path): - self.process_directory( - full_path, - renamed_dir, - self.top_level_var.get(), - self.subfolders_var.get(), - self.files_var.get(), - is_copy_mode, + full_path = os.path.join(self.directory, item) + work_items.append( + { + "path": full_path, + "is_dir": os.path.isdir(full_path), + "dest_dir": renamed_dir, + "rename_top_level": self.top_level_var.get(), + "rename_subfolders": self.subfolders_var.get(), + "rename_files": self.files_var.get(), + "is_copy_mode": is_copy_mode, + } + ) + + # Process items in parallel using a thread pool + with ThreadPoolExecutor(max_workers=CPU_COUNT) as executor: + futures = [] + for work in work_items: + if self.cancel_flag: + break + + if work["is_dir"]: + future = executor.submit( + self.process_directory, + work["path"], + work["dest_dir"], + work["rename_top_level"], + work["rename_subfolders"], + work["rename_files"], + work["is_copy_mode"], ) else: - self.process_file( - full_path, renamed_dir, self.files_var.get(), is_copy_mode + future = executor.submit( + self.process_file, + work["path"], + work["dest_dir"], + work["rename_files"], + work["is_copy_mode"], ) - except PermissionError: - self.show_error("Permission Error", f"Cannot access {item}.") - except shutil.Error as e: - self.show_error("Copy Error", f"Error copying {item}: {str(e)}") - except OSError as e: - self.show_error("OS Error", f"Error processing {item}: {str(e)}") + futures.append(future) + + # Wait for all tasks to complete + for future in futures: + try: + if self.cancel_flag: + executor.shutdown(wait=False, cancel_futures=True) + break + future.result() # This will raise any exceptions that occurred + except PermissionError as e: + self.show_error("Permission Error", str(e)) + except shutil.Error as e: + self.show_error("Copy Error", str(e)) + except OSError as e: + self.show_error("OS Error", str(e)) if not self.cancel_flag: msg = ( @@ -322,57 +359,81 @@ def process_directory( is_copy_mode, current_depth=0, ): - # First check if renaming is actually needed - orig_name = os.path.basename(src_dir) - new_dir_name = ( - make_fatx_compatible(orig_name, is_directory=True) - if rename_top_level - or (rename_subfolders and current_depth < self.depth_var.get()) - else orig_name - ) - - # Only use the new name if it's different and needs to be changed - needs_rename = not is_fatx_compatible(orig_name) - final_name = new_dir_name if needs_rename else orig_name - new_dir_path = os.path.join(dest_parent_dir, final_name) - - if is_copy_mode: - os.makedirs(new_dir_path, exist_ok=True) - elif needs_rename: # Only rename if necessary - os.rename(src_dir, new_dir_path) - src_dir = new_dir_path - - for root, dirs, files in os.walk(src_dir): - if self.cancel_flag: - return - rel_path = os.path.relpath(root, src_dir) - new_root = os.path.join( - new_dir_path if is_copy_mode else dest_parent_dir, rel_path + try: + # First check if renaming is actually needed + orig_name = os.path.basename(src_dir) + new_dir_name = ( + make_fatx_compatible(orig_name, is_directory=True) + if rename_top_level + or (rename_subfolders and current_depth < self.depth_var.get()) + else orig_name ) - if ( - rename_subfolders - and current_depth < self.depth_var.get() - and root != src_dir - ): - new_name = make_fatx_compatible( - os.path.basename(root), is_directory=True - ) - if new_name != os.path.basename(root): - new_root = os.path.join(os.path.dirname(new_root), new_name) - if not is_copy_mode: - os.rename(root, new_root) + # Only use the new name if it's different and needs to be changed + needs_rename = not is_fatx_compatible(orig_name) + final_name = new_dir_name if needs_rename else orig_name + new_dir_path = os.path.join(dest_parent_dir, final_name) if is_copy_mode: - os.makedirs(new_root, exist_ok=True) - - for file in files: - if self.cancel_flag: - return - src_file = os.path.join(root, file) - self.process_file(src_file, new_root, rename_files, is_copy_mode) + os.makedirs(new_dir_path, exist_ok=True) + elif needs_rename: # Only rename if necessary + os.rename(src_dir, new_dir_path) + src_dir = new_dir_path + + # Process directory contents + with ThreadPoolExecutor(max_workers=CPU_COUNT) as executor: + futures = [] + + for root, dirs, files in os.walk(src_dir): + if self.cancel_flag: + return + + rel_path = os.path.relpath(root, src_dir) + new_root = os.path.join( + new_dir_path if is_copy_mode else dest_parent_dir, rel_path + ) - break # Only process top level + if ( + rename_subfolders + and current_depth < self.depth_var.get() + and root != src_dir + ): + new_name = make_fatx_compatible( + os.path.basename(root), is_directory=True + ) + if new_name != os.path.basename(root): + new_root = os.path.join(os.path.dirname(new_root), new_name) + if not is_copy_mode: + os.rename(root, new_root) + + if is_copy_mode: + os.makedirs(new_root, exist_ok=True) + + # Process files in parallel + for file in files: + if self.cancel_flag: + return + src_file = os.path.join(root, file) + future = executor.submit( + self.process_file, + src_file, + new_root, + rename_files, + is_copy_mode, + ) + futures.append(future) + + break # Only process top level + + # Wait for all file operations to complete + for future in futures: + if self.cancel_flag: + executor.shutdown(wait=False, cancel_futures=True) + break + future.result() + + except Exception as e: + self.show_error("Directory Processing Error", str(e)) def process_file(self, src_file, dest_dir, rename_file, is_copy_mode): orig_name = os.path.basename(src_file) From 12eeec10d5714b4cd6c16b272c4d69c9936a0a1e Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 21:52:44 +0000 Subject: [PATCH 05/20] Jan 14, 2025, 1:52 PM --- .venv | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 .venv create mode 100644 requirements.txt diff --git a/.venv b/.venv new file mode 100644 index 0000000..cdeb514 --- /dev/null +++ b/.venv @@ -0,0 +1 @@ +FATX360-ncgj diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ + From cb2fb1248b289cada425bbf360b0a3cb093e725b Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 21:53:59 +0000 Subject: [PATCH 06/20] Jan 14, 2025, 1:53 PM --- fatx360.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/fatx360.py b/fatx360.py index 623845b..71b8ba2 100644 --- a/fatx360.py +++ b/fatx360.py @@ -1,11 +1,52 @@ -import multiprocessing -import os -import re -import shutil -import threading -import tkinter as tk -from concurrent.futures import ThreadPoolExecutor -from tkinter import filedialog, messagebox, ttk +try: + import tkinter as tk + from tkinter import filedialog, messagebox, ttk +except ImportError as e: + import platform + import sys + + def show_tkinter_installation_guide(): + system = platform.system().lower() + python_version = f"{sys.version_info.major}.{sys.version_info.minor}" + + guides = { + "darwin": f""" +Tkinter is not installed. To install it on macOS: + +1. Using Homebrew (recommended): + brew install python-tk@{python_version} + +2. Or download Python from python.org which includes Tkinter + https://www.python.org/downloads/ +""", + "linux": """ +Tkinter is not installed. To install it on Linux: + +For Ubuntu/Debian: + sudo apt-get update + sudo apt-get install python3-tk + +For Fedora: + sudo dnf install python3-tkinter + +For Other Distributions: + Please check your package manager for 'python3-tk' or 'tkinter' +""", + "windows": """ +Tkinter should be included with Python on Windows. +Try reinstalling Python from python.org and ensure you +don't uncheck tcl/tk during installation. +https://www.python.org/downloads/ +""", + } + + guide = guides.get(system, "Please install Tkinter for your operating system") + print("\nError: Unable to start FATX360 - Missing Tkinter\n") + print(guide) + print("\nAfter installing, try running this script again.\n") + sys.exit(1) + + show_tkinter_installation_guide() # Use 75% of available cores (minimum 2) to avoid overwhelming the system CPU_COUNT = max(2, int(multiprocessing.cpu_count() * 0.75)) From 19e29156f5317a568e20c04d9dc170054b21b7cf Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 22:48:05 +0000 Subject: [PATCH 07/20] Jan 14, 2025, 2:48 PM --- fatx360.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fatx360.py b/fatx360.py index 71b8ba2..96b8911 100644 --- a/fatx360.py +++ b/fatx360.py @@ -48,6 +48,14 @@ def show_tkinter_installation_guide(): show_tkinter_installation_guide() +# Add back the required imports +import multiprocessing +import os +import re +import shutil +import threading +from concurrent.futures import ThreadPoolExecutor + # Use 75% of available cores (minimum 2) to avoid overwhelming the system CPU_COUNT = max(2, int(multiprocessing.cpu_count() * 0.75)) From 97c5b47e8099715f0ab0c3a9bbe443db6255fd57 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 22:49:30 +0000 Subject: [PATCH 08/20] Jan 14, 2025, 2:49 PM --- fatx360.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/fatx360.py b/fatx360.py index 96b8911..616951f 100644 --- a/fatx360.py +++ b/fatx360.py @@ -106,9 +106,28 @@ def __init__(self, master=None): self.total_items = 0 self.processed_items = 0 self.cancel_flag = False - self.max_depth = 10 # Maximum depth for the slider + self.directory = None + self.max_depth = 1 # Start with a default of 1 self.create_widgets() + def get_directory_max_depth(self, directory): + """Calculate the maximum depth of the directory structure.""" + if not directory or not os.path.exists(directory): + return 1 + + max_depth = 0 + base_depth = directory.rstrip(os.sep).count(os.sep) + + for root, dirs, files in os.walk(directory): + if not dirs: # Skip if no subdirectories + continue + current_depth = root.count(os.sep) - base_depth + max_depth = max(max_depth, current_depth + 1) + if max_depth > 99: # Set a reasonable upper limit + return 99 + + return max(1, max_depth) # Ensure minimum depth of 1 + def create_widgets(self): self.create_menu() @@ -230,10 +249,22 @@ def create_menu(self): menubar.add_cascade(label="File", menu=file_menu) def select_directory(self): - self.directory = filedialog.askdirectory() - self.dir_entry.delete(0, tk.END) - self.dir_entry.insert(0, self.directory) - self.update_listbox() + selected_dir = filedialog.askdirectory() + if selected_dir: + self.directory = selected_dir + self.dir_entry.delete(0, tk.END) + self.dir_entry.insert(0, self.directory) + + # Update max depth based on directory structure + new_max_depth = self.get_directory_max_depth(self.directory) + self.max_depth = new_max_depth + + # Update the depth slider + self.depth_slider.configure(to=self.max_depth) + self.depth_var.set(min(self.depth_var.get(), self.max_depth)) + self.update_depth_label() + + self.update_listbox() def update_listbox(self): self.listbox.delete(0, tk.END) From 21c2dc10be6ad0dff9408aca17ec0b7c5b858204 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 22:51:17 +0000 Subject: [PATCH 09/20] Jan 14, 2025, 2:51 PM --- fatx360.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/fatx360.py b/fatx360.py index 616951f..81f25db 100644 --- a/fatx360.py +++ b/fatx360.py @@ -118,7 +118,12 @@ def get_directory_max_depth(self, directory): max_depth = 0 base_depth = directory.rstrip(os.sep).count(os.sep) - for root, dirs, files in os.walk(directory): + for root, dirs, _ in os.walk(directory): + # Remove ._ directories and other hidden directories from consideration + dirs[:] = [ + d for d in dirs if not d.startswith("._") and not d.startswith(".") + ] + if not dirs: # Skip if no subdirectories continue current_depth = root.count(os.sep) - base_depth @@ -254,23 +259,27 @@ def select_directory(self): self.directory = selected_dir self.dir_entry.delete(0, tk.END) self.dir_entry.insert(0, self.directory) + self.update_listbox() # This will now handle depth calculation and UI updates - # Update max depth based on directory structure + def update_listbox(self): + self.listbox.delete(0, tk.END) + try: + # Update max depth first new_max_depth = self.get_directory_max_depth(self.directory) self.max_depth = new_max_depth - - # Update the depth slider self.depth_slider.configure(to=self.max_depth) self.depth_var.set(min(self.depth_var.get(), self.max_depth)) self.update_depth_label() - self.update_listbox() - - def update_listbox(self): - self.listbox.delete(0, tk.END) - try: - for item in os.listdir(self.directory): + # Then populate listbox, ignoring ._ files + items = [ + item + for item in os.listdir(self.directory) + if not item.startswith("._") and not item.startswith(".") + ] + for item in sorted(items): self.listbox.insert(tk.END, item) + self.all_selected = False self.select_all_button.config(text="Select All") except PermissionError: From acf5beb74d46efd15707e39af7fb358ca6e2d48a Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 22:53:17 +0000 Subject: [PATCH 10/20] Jan 14, 2025, 2:53 PM --- fatx360.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/fatx360.py b/fatx360.py index 81f25db..b532254 100644 --- a/fatx360.py +++ b/fatx360.py @@ -171,13 +171,45 @@ def create_widgets(self): list_frame = ttk.Frame(self) list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + list_top_frame = ttk.Frame(list_frame) + list_top_frame.pack(fill=tk.X) + self.select_all_button = ttk.Button( - list_frame, text="Select All", command=self.toggle_select_all + list_top_frame, text="Select All", command=self.toggle_select_all + ) + self.select_all_button.pack(side=tk.LEFT) + + # Add log toggle button + self.show_log_var = tk.BooleanVar(value=False) + self.show_log_button = ttk.Checkbutton( + list_top_frame, + text="Show Log", + variable=self.show_log_var, + command=self.toggle_log_visibility, ) - self.select_all_button.pack(side=tk.TOP, anchor=tk.W) + self.show_log_button.pack(side=tk.RIGHT) + + # Create paned window to allow resizing between listbox and log + self.paned = ttk.PanedWindow(list_frame, orient=tk.VERTICAL) + self.paned.pack(fill=tk.BOTH, expand=True) + + # Add listbox to paned window + self.listbox = tk.Listbox(self.paned, selectmode=tk.MULTIPLE) + self.paned.add(self.listbox, weight=1) + + # Create log frame + self.log_frame = ttk.Frame(self.paned) - self.listbox = tk.Listbox(list_frame, selectmode=tk.MULTIPLE) - self.listbox.pack(fill=tk.BOTH, expand=True) + # Add log text widget with scrollbar + self.log_text = tk.Text(self.log_frame, height=6, wrap=tk.WORD) + log_scrollbar = ttk.Scrollbar(self.log_frame, command=self.log_text.yview) + self.log_text.configure(yscrollcommand=log_scrollbar.set) + + self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Initially hide log + self.toggle_log_visibility() options_frame = ttk.Frame(self) options_frame.pack(fill=tk.X, padx=10, pady=5) @@ -521,6 +553,9 @@ def process_directory( break future.result() + if needs_rename: + self.log_operation(f"Directory: {orig_name} → {final_name}") + except Exception as e: self.show_error("Directory Processing Error", str(e)) @@ -545,6 +580,11 @@ def process_file(self, src_file, dest_dir, rename_file, is_copy_mode): self.processed_items += 1 self.update_progress() + if needs_rename: + self.log_operation(f"File: {orig_name} → {final_name}") + elif is_copy_mode: + self.log_operation(f"Copying: {orig_name}") + def update_progress(self): progress_value = (self.processed_items / self.total_items) * 100 self.progress["value"] = progress_value @@ -591,6 +631,10 @@ def reset_interface(self): self.total_items = 0 self.processed_items = 0 + # Clear log + if hasattr(self, "log_text"): + self.log_text.delete(1.0, tk.END) + def show_error(self, title, message): self.master.after(0, lambda: messagebox.showerror(title, message)) @@ -600,6 +644,25 @@ def show_success(self, title, message): def show_info(self, title, message): self.master.after(0, lambda: messagebox.showinfo(title, message)) + def toggle_log_visibility(self): + if self.show_log_var.get(): + self.paned.add(self.log_frame, weight=1) + else: + self.paned.forget(self.log_frame) + + def log_operation(self, message): + """Add a message to the log with timestamp.""" + if not hasattr(self, "log_text"): + return + + from datetime import datetime + + timestamp = datetime.now().strftime("%H:%M:%S") + self.master.after( + 0, lambda: self.log_text.insert(tk.END, f"[{timestamp}] {message}\n") + ) + self.master.after(0, lambda: self.log_text.see(tk.END)) + root = tk.Tk() app = Application(master=root) From 9d713b0f2e86781c998fd0b6a7769c6fa81b6b1e Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 22:54:09 +0000 Subject: [PATCH 11/20] Jan 14, 2025, 2:54 PM --- fatx360.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/fatx360.py b/fatx360.py index b532254..f2a5855 100644 --- a/fatx360.py +++ b/fatx360.py @@ -645,10 +645,17 @@ def show_info(self, title, message): self.master.after(0, lambda: messagebox.showinfo(title, message)) def toggle_log_visibility(self): - if self.show_log_var.get(): - self.paned.add(self.log_frame, weight=1) - else: - self.paned.forget(self.log_frame) + try: + if self.show_log_var.get(): + self.paned.add(self.log_frame, weight=1) + else: + # Check if log_frame is currently managed by paned window + paned_slaves = self.paned.panes() + if self.log_frame in paned_slaves: + self.paned.forget(self.log_frame) + except tk.TclError: + # Ignore any Tcl errors during initialization + pass def log_operation(self, message): """Add a message to the log with timestamp.""" From a2697838ce6e66b13b05ee4a3186bfafe7b78809 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 22:55:12 +0000 Subject: [PATCH 12/20] Jan 14, 2025, 2:55 PM --- fatx360.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/fatx360.py b/fatx360.py index f2a5855..a529f8a 100644 --- a/fatx360.py +++ b/fatx360.py @@ -348,7 +348,7 @@ def rename_selected(self): messagebox.showwarning("No Selection", "Please select items to rename.") return - # Ask for confirmation if modifying in place + # Set destination based on operation mode if self.operation_mode.get() == "inplace": if not messagebox.askyesno( "Confirm Operation", @@ -357,12 +357,14 @@ def rename_selected(self): return dest_dir = self.directory else: + # Only ask for destination directory in copy mode dest_dir = filedialog.askdirectory( title="Select Destination Folder for Renamed Items" ) if not dest_dir: return + # Start the operation self.progress["value"] = 0 self.rename_button["state"] = "disabled" self.cancel_button["state"] = "normal" @@ -372,6 +374,7 @@ def rename_selected(self): self.processed_items = 0 self.update_progress_label() + # Start the operation in a new thread thread = threading.Thread( target=self.rename_items_thread, args=(selected_items, dest_dir) ) @@ -389,6 +392,9 @@ def count_total_items(self, items): return total def rename_items_thread(self, items, dest_dir): + # Enable cancel button only after thread starts + self.master.after(0, lambda: self.cancel_button.configure(state="normal")) + is_copy_mode = self.operation_mode.get() == "copy" if is_copy_mode: @@ -600,8 +606,8 @@ def cancel_operation(self): self.reset_interface() def finish_operation(self): - self.rename_button["state"] = "normal" - self.cancel_button["state"] = "disabled" + self.master.after(0, lambda: self.rename_button.configure(state="normal")) + self.master.after(0, lambda: self.cancel_button.configure(state="disabled")) self.cancel_flag = False self.reset_interface() From d9d686a53d60275f0c91c975e77066058e94347d Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 22:57:05 +0000 Subject: [PATCH 13/20] Jan 14, 2025, 2:57 PM --- fatx360.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/fatx360.py b/fatx360.py index a529f8a..e4ed6ca 100644 --- a/fatx360.py +++ b/fatx360.py @@ -349,25 +349,25 @@ def rename_selected(self): return # Set destination based on operation mode - if self.operation_mode.get() == "inplace": - if not messagebox.askyesno( - "Confirm Operation", - "This will modify the files in place. Are you sure you want to continue?", - ): - return - dest_dir = self.directory - else: + dest_dir = self.directory # Default to current directory for in-place mode + + if self.operation_mode.get() == "copy": # Only ask for destination directory in copy mode dest_dir = filedialog.askdirectory( title="Select Destination Folder for Renamed Items" ) - if not dest_dir: + if not dest_dir: # User cancelled directory selection + return + else: # in-place mode + if not messagebox.askyesno( + "Confirm Operation", + "This will modify the files in place. Are you sure you want to continue?", + ): return # Start the operation self.progress["value"] = 0 self.rename_button["state"] = "disabled" - self.cancel_button["state"] = "normal" self.cancel_flag = False self.total_items = self.count_total_items(selected_items) From cd0443783db3859402b9d2106c6cec55d2bf3c03 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 22:58:48 +0000 Subject: [PATCH 14/20] Jan 14, 2025, 2:58 PM --- fatx360.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/fatx360.py b/fatx360.py index e4ed6ca..ef8dfeb 100644 --- a/fatx360.py +++ b/fatx360.py @@ -348,22 +348,29 @@ def rename_selected(self): messagebox.showwarning("No Selection", "Please select items to rename.") return - # Set destination based on operation mode - dest_dir = self.directory # Default to current directory for in-place mode + # Verify we have a source directory + if not self.directory: + messagebox.showerror("Error", "Please select a source directory first.") + return - if self.operation_mode.get() == "copy": - # Only ask for destination directory in copy mode - dest_dir = filedialog.askdirectory( - title="Select Destination Folder for Renamed Items" - ) - if not dest_dir: # User cancelled directory selection - return - else: # in-place mode + # For in-place mode, just confirm and proceed + if self.operation_mode.get() == "inplace": if not messagebox.askyesno( "Confirm Operation", "This will modify the files in place. Are you sure you want to continue?", ): return + dest_dir = self.directory + else: + # For copy mode, use the RENAMED subfolder in source directory + dest_dir = os.path.join(self.directory, "RENAMED") + try: + os.makedirs(dest_dir, exist_ok=True) + except PermissionError: + messagebox.showerror( + "Permission Error", "Cannot create RENAMED directory." + ) + return # Start the operation self.progress["value"] = 0 From b162759d46955566586153b61de528faeaddede7 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 23:00:14 +0000 Subject: [PATCH 15/20] Jan 14, 2025, 3:00 PM --- fatx360.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/fatx360.py b/fatx360.py index ef8dfeb..b58a0a2 100644 --- a/fatx360.py +++ b/fatx360.py @@ -179,15 +179,25 @@ def create_widgets(self): ) self.select_all_button.pack(side=tk.LEFT) - # Add log toggle button + # Add log toggle button and clear button + log_controls = ttk.Frame(list_top_frame) + log_controls.pack(side=tk.RIGHT) + self.show_log_var = tk.BooleanVar(value=False) self.show_log_button = ttk.Checkbutton( - list_top_frame, + log_controls, text="Show Log", variable=self.show_log_var, command=self.toggle_log_visibility, ) - self.show_log_button.pack(side=tk.RIGHT) + self.show_log_button.pack(side=tk.LEFT) + + self.clear_log_button = ttk.Button( + log_controls, + text="Clear Log", + command=lambda: self.log_text.delete(1.0, tk.END), + ) + self.clear_log_button.pack(side=tk.LEFT, padx=(5, 0)) # Create paned window to allow resizing between listbox and log self.paned = ttk.PanedWindow(list_frame, orient=tk.VERTICAL) @@ -644,10 +654,6 @@ def reset_interface(self): self.total_items = 0 self.processed_items = 0 - # Clear log - if hasattr(self, "log_text"): - self.log_text.delete(1.0, tk.END) - def show_error(self, title, message): self.master.after(0, lambda: messagebox.showerror(title, message)) From 4c64a55d183287aa0cd3feee2a6643308c00d781 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 23:02:36 +0000 Subject: [PATCH 16/20] Jan 14, 2025, 3:02 PM --- fatx360.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/fatx360.py b/fatx360.py index b58a0a2..c377c3a 100644 --- a/fatx360.py +++ b/fatx360.py @@ -504,6 +504,10 @@ def process_directory( current_depth=0, ): try: + # Skip hidden directories + if os.path.basename(src_dir).startswith("."): + return + # First check if renaming is actually needed orig_name = os.path.basename(src_dir) new_dir_name = ( @@ -514,7 +518,7 @@ def process_directory( ) # Only use the new name if it's different and needs to be changed - needs_rename = not is_fatx_compatible(orig_name) + needs_rename = rename_top_level and not is_fatx_compatible(orig_name) final_name = new_dir_name if needs_rename else orig_name new_dir_path = os.path.join(dest_parent_dir, final_name) @@ -532,6 +536,11 @@ def process_directory( if self.cancel_flag: return + # Skip hidden directories + dirs[:] = [d for d in dirs if not d.startswith(".")] + # Skip hidden files + files = [f for f in files if not f.startswith(".")] + rel_path = os.path.relpath(root, src_dir) new_root = os.path.join( new_dir_path if is_copy_mode else dest_parent_dir, rel_path @@ -583,6 +592,10 @@ def process_directory( self.show_error("Directory Processing Error", str(e)) def process_file(self, src_file, dest_dir, rename_file, is_copy_mode): + # Skip hidden files + if os.path.basename(src_file).startswith("."): + return + orig_name = os.path.basename(src_file) new_name = ( make_fatx_compatible(orig_name, is_directory=False) @@ -592,22 +605,25 @@ def process_file(self, src_file, dest_dir, rename_file, is_copy_mode): # Only proceed with rename if the name actually needs to change needs_rename = rename_file and not is_fatx_compatible(orig_name) - final_name = new_name if needs_rename else orig_name - new_path = os.path.join(dest_dir, final_name) - - if is_copy_mode: - shutil.copy2(src_file, new_path) - elif needs_rename: # Only rename if necessary - os.rename(src_file, new_path) + if needs_rename: + final_name = new_name + new_path = os.path.join(dest_dir, final_name) - self.processed_items += 1 - self.update_progress() + if is_copy_mode: + shutil.copy2(src_file, new_path) + else: + os.rename(src_file, new_path) - if needs_rename: self.log_operation(f"File: {orig_name} → {final_name}") elif is_copy_mode: + # In copy mode, copy even if no rename needed + new_path = os.path.join(dest_dir, orig_name) + shutil.copy2(src_file, new_path) self.log_operation(f"Copying: {orig_name}") + self.processed_items += 1 + self.update_progress() + def update_progress(self): progress_value = (self.processed_items / self.total_items) * 100 self.progress["value"] = progress_value From 125ece1d19432ecfa103f092f31f0ad53a226122 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 23:08:16 +0000 Subject: [PATCH 17/20] Jan 14, 2025, 3:08 PM --- requirements.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8b13789..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ - From 2874e670e6fe4c13871ad17b4fc6d893c7d63f82 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 23:08:24 +0000 Subject: [PATCH 18/20] Jan 14, 2025, 3:08 PM --- fatx360.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatx360.py b/fatx360.py index c377c3a..a0ce51b 100644 --- a/fatx360.py +++ b/fatx360.py @@ -99,7 +99,7 @@ class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) self.master = master - self.master.title("FATX360 v1.3") + self.master.title("FATX360 v1.4") self.master.geometry("500x550") self.pack(fill=tk.BOTH, expand=True) self.all_selected = False From 7fef1864848249a4fe8a0cdc596b66646b9fe680 Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Tue, 14 Jan 2025 23:08:41 +0000 Subject: [PATCH 19/20] Jan 14, 2025, 3:08 PM --- fatx360.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatx360.py b/fatx360.py index a0ce51b..d7bae0a 100644 --- a/fatx360.py +++ b/fatx360.py @@ -99,7 +99,7 @@ class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) self.master = master - self.master.title("FATX360 v1.4") + self.master.title("FATX360 v1.4") # Update for major features self.master.geometry("500x550") self.pack(fill=tk.BOTH, expand=True) self.all_selected = False From 7cdcfa2b1e28740215b97c9243cf43c40590bd9a Mon Sep 17 00:00:00 2001 From: Chris Farrell Date: Wed, 15 Jan 2025 00:23:35 +0000 Subject: [PATCH 20/20] Jan 14, 2025, 4:23 PM --- .venv | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .venv diff --git a/.venv b/.venv deleted file mode 100644 index cdeb514..0000000 --- a/.venv +++ /dev/null @@ -1 +0,0 @@ -FATX360-ncgj