Skip to content

Commit ff4cc07

Browse files
authored
Create File-Size-Organizer.py
1 parent 521b38b commit ff4cc07

1 file changed

Lines changed: 345 additions & 0 deletions

File tree

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
import os
2+
import json
3+
import shutil
4+
import tkinter as tk
5+
from tkinter import ttk, filedialog, messagebox
6+
from tkinterdnd2 import DND_FILES, TkinterDnD
7+
import sv_ttk
8+
from threading import Thread
9+
from watchdog.observers import Observer
10+
from watchdog.events import FileSystemEventHandler
11+
12+
# =========================
13+
# Helpers
14+
# =========================
15+
CONFIG_FILE = "folders_data.json"
16+
17+
def load_folders():
18+
if os.path.exists(CONFIG_FILE):
19+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
20+
return json.load(f)
21+
return []
22+
23+
def save_folders():
24+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
25+
json.dump(folders_data, f, ensure_ascii=False, indent=4)
26+
27+
def set_status(msg):
28+
status_var.set(msg)
29+
root.update_idletasks()
30+
31+
def sanitize_folder_name(name):
32+
return "".join(c for c in name if c not in r'<>:"/\|?*')
33+
34+
def format_size(size_bytes):
35+
if size_bytes < 1024:
36+
return f"{size_bytes} B"
37+
elif size_bytes < 1024**2:
38+
return f"{size_bytes/1024:.2f} KB"
39+
elif size_bytes < 1024**3:
40+
return f"{size_bytes/1024**2:.2f} MB"
41+
else:
42+
return f"{size_bytes/1024**3:.2f} GB"
43+
44+
def categorize_file(size):
45+
if size < 1024*1024:
46+
return "Small_1MB"
47+
elif size < 10*1024*1024:
48+
return "Medium_1-10MB"
49+
else:
50+
return "Large_10MB_plus"
51+
52+
# =========================
53+
# App Setup
54+
# =========================
55+
root = TkinterDnD.Tk()
56+
root.title("📂 File Size Organizer Pro - Filter & Sort")
57+
root.geometry("1300x620")
58+
59+
# =========================
60+
# Globals
61+
# =========================
62+
folders_data = load_folders()
63+
last_operation = []
64+
combined_files = []
65+
filtered_files = []
66+
observer = None
67+
current_filter = tk.StringVar(value="All")
68+
current_sort = tk.StringVar(value="Name")
69+
70+
# =========================
71+
# Folder & File Functions
72+
# =========================
73+
def add_folder(path):
74+
path = path.strip()
75+
76+
# 1️⃣ Empty validation
77+
if not path:
78+
messagebox.showwarning("Invalid Folder", "Please select or enter a folder path.")
79+
return
80+
81+
path = os.path.abspath(path)
82+
83+
# 2️⃣ Directory validation
84+
if not os.path.isdir(path):
85+
messagebox.showwarning("Invalid Folder", "The selected path is not a valid folder.")
86+
return
87+
88+
# 3️⃣ Duplicate validation
89+
for folder in folders_data:
90+
if os.path.abspath(folder) == path:
91+
messagebox.showinfo("Already Added", "This folder is already being monitored.")
92+
return
93+
94+
# 4️⃣ Add folder
95+
folders_data.append(path)
96+
save_folders()
97+
start_watcher(path)
98+
refresh_combined_preview()
99+
set_status(f"Added folder: {path}")
100+
101+
def remove_folder(path):
102+
path = os.path.abspath(path.strip())
103+
104+
for folder in folders_data:
105+
if os.path.abspath(folder) == path:
106+
folders_data.remove(folder)
107+
save_folders()
108+
refresh_combined_preview()
109+
set_status(f"Removed folder: {path}")
110+
return
111+
112+
messagebox.showwarning("Not Found", "Folder not found in the list.")
113+
114+
def scan_folder(folder_path):
115+
folder_path = os.path.abspath(folder_path)
116+
files = []
117+
if not os.path.exists(folder_path):
118+
return files
119+
120+
for f in os.listdir(folder_path):
121+
path = os.path.join(folder_path, f)
122+
if os.path.isfile(path):
123+
try:
124+
size = os.path.getsize(path)
125+
except (FileNotFoundError, PermissionError):
126+
continue # file moved or locked, safely skip
127+
128+
category = sanitize_folder_name(categorize_file(size))
129+
files.append((folder_path, f, size, category))
130+
131+
return files
132+
133+
def refresh_combined_preview():
134+
global combined_files
135+
combined_files = []
136+
for folder in folders_data:
137+
combined_files.extend(scan_folder(folder))
138+
apply_filter_sort()
139+
140+
# =========================
141+
# Filter & Sort Functions
142+
# =========================
143+
def apply_filter_sort():
144+
global filtered_files
145+
# Filter
146+
if current_filter.get() == "All":
147+
filtered_files = combined_files.copy()
148+
else:
149+
filtered_files = [f for f in combined_files if f[3] == current_filter.get()]
150+
# Sort
151+
sort_key = current_sort.get()
152+
if sort_key == "Name":
153+
filtered_files.sort(key=lambda x: x[1].lower())
154+
elif sort_key == "Size":
155+
filtered_files.sort(key=lambda x: x[2])
156+
elif sort_key == "Folder":
157+
filtered_files.sort(key=lambda x: x[0].lower())
158+
elif sort_key == "Category":
159+
filtered_files.sort(key=lambda x: x[3].lower())
160+
update_file_tree()
161+
162+
def update_file_tree():
163+
file_tree.delete(*file_tree.get_children())
164+
counts = {"Small_1MB":0, "Medium_1-10MB":0, "Large_10MB_plus":0}
165+
for folder, f, size, category in filtered_files:
166+
file_tree.insert("", "end", values=(folder, f, format_size(size), category))
167+
if category in counts:
168+
counts[category] +=1
169+
total_files_var.set(f"Total Files: {len(filtered_files)} | Small: {counts['Small_1MB']} | Medium: {counts['Medium_1-10MB']} | Large: {counts['Large_10MB_plus']}")
170+
171+
# =========================
172+
# File Operations
173+
# =========================
174+
def organize_files_thread():
175+
global watcher_paused
176+
watcher_paused = True
177+
progress_var.set(0)
178+
179+
if not combined_files:
180+
watcher_paused = False
181+
return
182+
183+
global last_operation
184+
last_operation = []
185+
total = len(combined_files)
186+
187+
for idx, (folder, f, size, category) in enumerate(combined_files, start=1):
188+
src = os.path.join(folder, f)
189+
dst_folder = os.path.join(folder, category)
190+
os.makedirs(dst_folder, exist_ok=True)
191+
dst = os.path.join(dst_folder, f)
192+
193+
try:
194+
shutil.move(src, dst)
195+
last_operation.append((dst, src))
196+
except Exception:
197+
pass
198+
199+
progress_var.set(int(idx / total * 100))
200+
root.update_idletasks()
201+
202+
watcher_paused = False
203+
refresh_combined_preview()
204+
set_status("Files organized successfully!")
205+
206+
def organize_files():
207+
Thread(target=organize_files_thread, daemon=True).start()
208+
209+
def undo_last_operation():
210+
if not last_operation:
211+
messagebox.showinfo("Nothing to undo", "No operation to undo.")
212+
return
213+
for dst, src in reversed(last_operation):
214+
if os.path.exists(dst):
215+
shutil.move(dst, src)
216+
last_operation.clear()
217+
refresh_combined_preview()
218+
set_status("Undo completed!")
219+
220+
# =========================
221+
# Drag & Drop
222+
# =========================
223+
def drop(event):
224+
paths = root.tk.splitlist(event.data)
225+
for path in paths:
226+
if os.path.isdir(path):
227+
add_folder(path)
228+
229+
# =========================
230+
# Watchdog Handler
231+
# =========================
232+
class FolderEventHandler(FileSystemEventHandler):
233+
def on_any_event(self, event):
234+
if watcher_paused:
235+
return
236+
root.after(200, refresh_combined_preview)
237+
238+
def start_watcher(folder_path):
239+
global observer
240+
if observer is None:
241+
observer = Observer()
242+
observer.start()
243+
handler = FolderEventHandler()
244+
observer.schedule(handler, folder_path, recursive=True)
245+
246+
# =========================
247+
# GUI Setup
248+
# =========================
249+
main_frame = ttk.Frame(root, padding=20)
250+
main_frame.pack(expand=True, fill="both")
251+
252+
ttk.Label(main_frame, text="📂 File Size Organizer Pro - Filter & Sort", font=("Segoe UI", 22, "bold")).pack(pady=(0,5))
253+
ttk.Label(main_frame, text="Drag folders or add manually. Files auto-update in real-time.", font=("Segoe UI", 12)).pack(pady=(0,10))
254+
255+
# Folder selection and management
256+
folder_frame = ttk.LabelFrame(main_frame, text="Folders", padding=10)
257+
folder_frame.pack(fill="x", pady=5)
258+
259+
folder_entry = tk.Entry(folder_frame, width=80)
260+
folder_entry.grid(row=0, column=0, padx=5)
261+
262+
def browse_folder():
263+
path = filedialog.askdirectory()
264+
if path:
265+
folder_entry.delete(0, tk.END)
266+
folder_entry.insert(0, path)
267+
268+
ttk.Button(folder_frame, text="Browse", command=browse_folder).grid(row=0, column=1, padx=5)
269+
ttk.Button(folder_frame, text="Add Folder", style="Manage.TButton", command=lambda:add_folder(folder_entry.get().strip())).grid(row=0, column=2, padx=5)
270+
ttk.Button(folder_frame, text="Remove Folder", style="Undo.TButton", command=lambda: remove_folder(folder_entry.get().strip())).grid(row=0, column=3, padx=5)
271+
272+
# Buttons styles
273+
ttk.Style().configure("Organize.TButton", foreground="black", background="#4CAF50")
274+
ttk.Style().configure("Undo.TButton", foreground="black", background="#FF9800")
275+
ttk.Style().configure("Manage.TButton", foreground="black", background="#2196F3")
276+
277+
ttk.Button(folder_frame, text="📂 Organize Files", style="Organize.TButton", command=organize_files).grid(row=0, column=4, padx=5)
278+
ttk.Button(folder_frame, text="↩️ Undo Last", style="Undo.TButton", command=undo_last_operation).grid(row=0, column=5, padx=5)
279+
280+
# Filter & Sort Panel
281+
filter_sort_frame = ttk.LabelFrame(main_frame, text="Filter & Sort", padding=10)
282+
filter_sort_frame.pack(fill="x", pady=5)
283+
284+
ttk.Label(filter_sort_frame, text="Filter by Category:").grid(row=0, column=0, padx=5)
285+
filter_combo = ttk.Combobox(filter_sort_frame, state="readonly", width=20, textvariable=current_filter)
286+
filter_combo['values'] = ["All", "Small_1MB", "Medium_1-10MB", "Large_10MB_plus"]
287+
filter_combo.current(0)
288+
filter_combo.grid(row=0, column=1, padx=5)
289+
filter_combo.bind("<<ComboboxSelected>>", lambda e: apply_filter_sort())
290+
291+
ttk.Label(filter_sort_frame, text="Sort by:").grid(row=0, column=2, padx=5)
292+
sort_combo = ttk.Combobox(filter_sort_frame, state="readonly", width=20, textvariable=current_sort)
293+
sort_combo['values'] = ["Name", "Size", "Folder", "Category"]
294+
sort_combo.current(0)
295+
sort_combo.grid(row=0, column=3, padx=5)
296+
sort_combo.bind("<<ComboboxSelected>>", lambda e: apply_filter_sort())
297+
298+
# File preview Treeview
299+
tree_frame = ttk.Frame(main_frame)
300+
tree_frame.pack(expand=True, fill="both", pady=10)
301+
columns = ("folder", "name", "size", "category")
302+
file_tree = ttk.Treeview(tree_frame, columns=columns, show="headings")
303+
file_tree.heading("folder", text="Folder")
304+
file_tree.heading("name", text="File Name")
305+
file_tree.heading("size", text="Size")
306+
file_tree.heading("category", text="Category")
307+
file_tree.column("folder", width=300)
308+
file_tree.column("name", width=400)
309+
file_tree.column("size", width=100)
310+
file_tree.column("category", width=150)
311+
file_tree.pack(expand=True, fill="both", side="left")
312+
313+
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=file_tree.yview)
314+
scrollbar.pack(side="right", fill="y")
315+
file_tree.configure(yscrollcommand=scrollbar.set)
316+
317+
# File counts
318+
total_files_var = tk.StringVar(value="Total Files: 0 | Small: 0 | Medium: 0 | Large: 0")
319+
ttk.Label(main_frame, textvariable=total_files_var, font=("Segoe UI", 12)).pack()
320+
321+
# Progress bar
322+
progress_var = tk.IntVar()
323+
progress_bar = ttk.Progressbar(main_frame, orient="horizontal", mode="determinate", maximum=100, variable=progress_var)
324+
progress_bar.pack(fill="x", pady=5)
325+
326+
# Drag & Drop binding
327+
root.drop_target_register(DND_FILES)
328+
root.dnd_bind('<<Drop>>', drop)
329+
330+
# Status Bar
331+
status_var = tk.StringVar(value="Ready")
332+
ttk.Label(root, textvariable=status_var, anchor="w").pack(side="bottom", fill="x")
333+
334+
# Initial load and start watchers
335+
for folder in folders_data:
336+
start_watcher(folder)
337+
refresh_combined_preview()
338+
339+
# Run App
340+
try:
341+
root.mainloop()
342+
finally:
343+
if observer:
344+
observer.stop()
345+
observer.join()

0 commit comments

Comments
 (0)