Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import os
- import shutil
- import subprocess
- import threading
- import queue
- import customtkinter as ctk
- from tkinter import filedialog, messagebox
- ctk.set_appearance_mode("System")
- ctk.set_default_color_theme("blue")
- class SubKeywordRow(ctk.CTkFrame):
- def __init__(self, master, sub_keyword, on_remove, on_move_up, on_move_down):
- super().__init__(master)
- self.sub_keyword = sub_keyword
- self.synonyms = []
- self.on_remove = on_remove
- self.on_move_up = on_move_up
- self.on_move_down = on_move_down
- self.label = ctk.CTkLabel(self, text=sub_keyword, width=180, anchor="w")
- self.label.grid(row=0, column=0, padx=(5, 0), sticky="w")
- ctk.CTkButton(self, text="⬆️", width=30, command=self.on_move_up).grid(row=0, column=1, padx=2)
- ctk.CTkButton(self, text="⬇️", width=30, command=self.on_move_down).grid(row=0, column=2, padx=2)
- ctk.CTkButton(self, text="➕ Syn", width=60, command=self.add_synonym).grid(row=0, column=3, padx=2)
- ctk.CTkButton(self, text="❌", width=30, fg_color="red", command=self.on_remove).grid(row=0, column=4, padx=2)
- self.syn_label = ctk.CTkLabel(self, text="", anchor="w")
- self.syn_label.grid(row=1, column=0, columnspan=5, sticky="w", padx=5)
- def add_synonym(self):
- popup = ctk.CTkInputDialog(title="Add Synonym", text=f"Synonym for '{self.sub_keyword}':")
- value = popup.get_input()
- if value and value not in self.synonyms:
- self.synonyms.append(value)
- self.update_syn_label()
- def update_syn_label(self):
- if self.synonyms:
- self.syn_label.configure(text="Synonyms: " + ", ".join(self.synonyms))
- else:
- self.syn_label.configure(text="")
- def get_data(self):
- return {
- "main": self.sub_keyword,
- "match": [self.sub_keyword.lower()] + [s.lower() for s in self.synonyms]
- }
- class KeywordRow(ctk.CTkFrame):
- def __init__(self, master, keyword, on_update, move_up_callback, move_down_callback):
- super().__init__(master)
- self.keyword = keyword
- self.synonyms = []
- self.sub_rows = []
- self.on_update = on_update
- self.move_up_callback = move_up_callback
- self.move_down_callback = move_down_callback
- self.pack(fill="x", pady=4, padx=5)
- # Top Controls
- top_frame = ctk.CTkFrame(self)
- top_frame.pack(fill="x")
- ctk.CTkLabel(top_frame, text=keyword, width=180, anchor="w").pack(side="left")
- ctk.CTkButton(top_frame, text="⬆️", width=30, command=self.move_up_callback).pack(side="left", padx=2)
- ctk.CTkButton(top_frame, text="⬇️", width=30, command=self.move_down_callback).pack(side="left", padx=2)
- ctk.CTkButton(top_frame, text="➕ Sub", width=60, command=self.add_subkeyword).pack(side="left", padx=2)
- ctk.CTkButton(top_frame, text="➕ Syn", width=60, command=self.add_synonym).pack(side="left", padx=2)
- ctk.CTkButton(top_frame, text="❌", width=30, fg_color="red", command=self.remove_self).pack(side="left", padx=2)
- # Synonyms display
- self.syn_label = ctk.CTkLabel(self, text="", anchor="w")
- self.syn_label.pack(fill="x", padx=5)
- self.sub_frame = None # Will be created when subkeywords are added
- def add_synonym(self):
- popup = ctk.CTkInputDialog(title="Add Synonym", text=f"Synonym for '{self.keyword}':")
- value = popup.get_input()
- if value and value not in self.synonyms:
- self.synonyms.append(value)
- self.update_syn_label()
- def update_syn_label(self):
- if self.synonyms:
- self.syn_label.configure(text="Synonyms: " + ", ".join(self.synonyms))
- else:
- self.syn_label.configure(text="")
- def add_subkeyword(self):
- popup = ctk.CTkInputDialog(title="Add Sub-keyword", text=f"Sub-keyword for '{self.keyword}':")
- value = popup.get_input()
- if value and value not in [row.sub_keyword for row in self.sub_rows]:
- self.create_subkeyword_row(value)
- self.on_update()
- def create_subkeyword_row(self, subkeyword):
- if not self.sub_frame:
- self.sub_frame = ctk.CTkFrame(self)
- self.sub_frame.pack(fill="x")
- def remove():
- self.sub_rows.remove(row)
- row.destroy()
- self.on_update()
- def move_up():
- idx = self.sub_rows.index(row)
- if idx > 0:
- self.sub_rows[idx], self.sub_rows[idx - 1] = self.sub_rows[idx - 1], self.sub_rows[idx]
- self.refresh_subkeywords()
- self.on_update()
- def move_down():
- idx = self.sub_rows.index(row)
- if idx < len(self.sub_rows) - 1:
- self.sub_rows[idx], self.sub_rows[idx + 1] = self.sub_rows[idx + 1], self.sub_rows[idx]
- self.refresh_subkeywords()
- self.on_update()
- row = SubKeywordRow(self.sub_frame, subkeyword, remove, move_up, move_down)
- self.sub_rows.append(row)
- row.pack(fill="x", pady=2, padx=10)
- def refresh_subkeywords(self):
- if self.sub_frame:
- for widget in self.sub_frame.winfo_children():
- widget.pack_forget()
- for row in self.sub_rows:
- row.pack(fill="x", pady=2, padx=10)
- def remove_self(self):
- self.destroy()
- self.on_update()
- def get_data(self):
- return {
- "match": [self.keyword.lower()] + [s.lower() for s in self.synonyms],
- "subs": [row.get_data() for row in self.sub_rows]
- }
- import json
- from tkinter import filedialog
- class KeywordManager(ctk.CTkFrame):
- def __init__(self, master):
- super().__init__(master)
- self.rows = []
- # Top input + Add
- top_frame = ctk.CTkFrame(self)
- top_frame.pack(fill="x", pady=(0, 5))
- self.input_var = ctk.StringVar()
- self.entry = ctk.CTkEntry(top_frame, textvariable=self.input_var)
- self.entry.pack(side="left", fill="x", expand=True, padx=(0, 8))
- self.add_btn = ctk.CTkButton(top_frame, text="➕ Add", command=self.add_main_keyword)
- self.add_btn.pack(side="left")
- # Save/Load Buttons
- file_frame = ctk.CTkFrame(self)
- file_frame.pack(fill="x", pady=(0, 5))
- ctk.CTkButton(file_frame, text="💾 Save Profile", command=self.save_profile).pack(side="left", padx=4)
- ctk.CTkButton(file_frame, text="📂 Load Profile", command=self.load_profile).pack(side="left", padx=4)
- # Scroll Frame
- self.scroll_frame = ctk.CTkScrollableFrame(self, height=420, fg_color=self.cget("fg_color"))
- self.scroll_frame.pack(fill="both", expand=True, pady=5)
- def add_main_keyword(self):
- keyword = self.input_var.get().strip()
- if keyword and not any(row.keyword == keyword for row in self.rows):
- self.create_keyword_row(keyword)
- self.input_var.set("")
- def create_keyword_row(self, keyword, synonyms=None, subs=None):
- def on_update():
- self.safe_refresh_list()
- row = None # declare here for closure access
- def move_up():
- if row in self.rows:
- idx = self.rows.index(row)
- self.move_row(idx, -1)
- def move_down():
- if row in self.rows:
- idx = self.rows.index(row)
- self.move_row(idx, 1)
- row = KeywordRow(self.scroll_frame, keyword, on_update, move_up, move_down)
- if synonyms:
- row.synonyms = synonyms
- row.update_syn_label()
- if subs:
- for sub in subs:
- row.create_subkeyword_row(sub["main"])
- row.sub_rows[-1].synonyms = sub.get("match", [])[1:] # skip main
- row.sub_rows[-1].update_syn_label()
- self.rows.append(row)
- row.pack(fill="x", pady=4, padx=5)
- def move_row(self, index, direction):
- new_index = index + direction
- if 0 <= new_index < len(self.rows):
- self.rows[index], self.rows[new_index] = self.rows[new_index], self.rows[index]
- self.refresh_list()
- def safe_refresh_list(self):
- self.rows = [row for row in self.rows if row.winfo_exists()]
- for widget in self.scroll_frame.winfo_children():
- widget.pack_forget()
- for row in self.rows:
- row.pack(fill="x", pady=4, padx=5)
- def refresh_list(self):
- self.safe_refresh_list()
- def get_keyword_map(self):
- self.safe_refresh_list()
- return {row.keyword.lower(): row.get_data() for row in self.rows}
- def save_profile(self):
- path = filedialog.asksaveasfilename(
- defaultextension=".json",
- filetypes=[("JSON files", "*.json")],
- title="Save Keyword Profile"
- )
- if path:
- try:
- with open(path, "w", encoding="utf-8") as f:
- json.dump(self.get_keyword_map(), f, indent=2)
- messagebox.showinfo("✅ Saved", "Profile saved successfully.")
- except Exception as e:
- messagebox.showerror("Error", f"Failed to save profile:\n{e}")
- def load_profile(self):
- path = filedialog.askopenfilename(
- filetypes=[("JSON files", "*.json")],
- title="Load Keyword Profile"
- )
- if path:
- try:
- with open(path, "r", encoding="utf-8") as f:
- data = json.load(f)
- # Clear old rows
- for row in self.rows:
- row.destroy()
- self.rows.clear()
- # Restore from profile
- for keyword, value in data.items():
- synonyms = value.get("match", [])[1:] # skip main keyword
- subs = value.get("subs", [])
- self.create_keyword_row(keyword, synonyms, subs)
- self.safe_refresh_list()
- messagebox.showinfo("✅ Loaded", "Profile loaded successfully.")
- except Exception as e:
- messagebox.showerror("Error", f"Failed to load profile:\n{e}")
- class ImageSorterApp:
- def __init__(self, root):
- self.root = root
- self.root.title("🧠 Smart Image Sorter")
- self.root.geometry("800x1300") # Increased height for better keyword visibility
- self.root.resizable(False, False)
- self.queue = queue.Queue()
- self.frame = ctk.CTkFrame(root, corner_radius=10)
- self.frame.pack(padx=20, pady=20, fill="both", expand=True)
- # ========== Input Folders ==========
- ctk.CTkLabel(self.frame, text="📂 Input Folders").grid(row=0, column=0, sticky="w", pady=(0, 5))
- input_frame = ctk.CTkFrame(self.frame)
- input_frame.grid(row=0, column=1, columnspan=2, sticky="ew", padx=10)
- input_frame.grid_columnconfigure(0, weight=1)
- self.input_listbox = ctk.CTkTextbox(input_frame, height=100)
- self.input_listbox.grid(row=0, column=0, sticky="ew")
- input_buttons_frame = ctk.CTkFrame(input_frame)
- input_buttons_frame.grid(row=1, column=0, pady=(5, 0), sticky="ew")
- ctk.CTkButton(input_buttons_frame, text="➕ Add Folder", command=self.add_input_folder).pack(side="left", padx=(0, 5))
- ctk.CTkButton(input_buttons_frame, text="🗑️ Clear All", command=self.clear_input_folders).pack(side="left")
- self.include_subfolders = ctk.CTkCheckBox(self.frame, text="Include Subfolders")
- self.include_subfolders.grid(row=1, column=1, columnspan=2, sticky="w", padx=10)
- self.input_folders = []
- # ========== Output Folder ==========
- ctk.CTkLabel(self.frame, text="📁 Output Folder").grid(row=2, column=0, sticky="w", pady=(10, 5))
- self.output_entry = ctk.CTkEntry(self.frame)
- self.output_entry.grid(row=2, column=1, padx=10, sticky="ew")
- ctk.CTkButton(self.frame, text="Browse", command=self.browse_output).grid(row=2, column=2, sticky="w")
- # ========== Keywords & Sub-keywords ==========
- ctk.CTkLabel(self.frame, text="🔖 Keywords + Sub-keywords").grid(row=3, column=0, sticky="nw", pady=(20, 5))
- self.keyword_manager = KeywordManager(self.frame)
- self.keyword_manager.grid(row=3, column=1, columnspan=2, sticky="nsew", pady=(10, 0))
- # Save/Load Buttons
- button_row = ctk.CTkFrame(self.frame)
- button_row.grid(row=4, column=1, columnspan=2, sticky="w", pady=(5, 0), padx=5)
- ctk.CTkButton(button_row, text="💾 Save Profile", command=self.keyword_manager.save_profile).pack(side="left", padx=5)
- ctk.CTkButton(button_row, text="📂 Load Profile", command=self.keyword_manager.load_profile).pack(side="left", padx=5)
- # ========== Progress Bar ==========
- self.progress = ctk.CTkProgressBar(self.frame)
- self.progress.grid(row=5, column=0, columnspan=3, pady=(30, 10), sticky="ew", padx=5)
- self.progress.set(0.0)
- # ========== Start Button ==========
- self.start_btn = ctk.CTkButton(self.frame, text="🚀 Start Sorting", command=self.start_sorting)
- self.start_btn.grid(row=6, column=0, columnspan=3, pady=(0, 10))
- # Make center column expandable
- self.frame.grid_columnconfigure(1, weight=1)
- self.frame.grid_rowconfigure(3, weight=1)
- def add_input_folder(self):
- initial_dir = os.path.dirname(self.last_folder) if hasattr(self, 'last_folder') and os.path.isdir(self.last_folder) else os.getcwd()
- folder = filedialog.askdirectory(title="Select Input Folder", initialdir=initial_dir)
- if folder:
- self.last_folder = folder
- if folder not in self.input_folders:
- self.input_folders.append(folder)
- self.update_input_display()
- def clear_input_folders(self):
- self.input_folders.clear()
- self.update_input_display()
- def update_input_display(self):
- self.input_listbox.delete("1.0", "end")
- for folder in self.input_folders:
- self.input_listbox.insert("end", folder + "\n")
- def browse_output(self):
- folder = filedialog.askdirectory()
- if folder:
- self.output_entry.delete(0, "end")
- self.output_entry.insert(0, folder)
- def start_sorting(self):
- input_folders = self.input_folders
- output_folder = self.output_entry.get()
- include_sub = self.include_subfolders.get()
- keyword_map = self.keyword_manager.get_keyword_map()
- if not input_folders:
- messagebox.showerror("Error", "Please select at least one input folder.")
- return
- if not output_folder:
- messagebox.showerror("Error", "Please select an output folder.")
- return
- if output_folder in input_folders:
- messagebox.showerror("Error", "Output folder must be different from input folders.")
- return
- if not keyword_map:
- messagebox.showerror("Error", "Please enter at least one keyword.")
- return
- self.start_btn.configure(state="disabled")
- self.progress.set(0)
- threading.Thread(target=self.sort_thread, args=(input_folders, output_folder, keyword_map, include_sub), daemon=True).start()
- self.root.after(100, self.check_queue)
- def check_queue(self):
- try:
- while True:
- value = self.queue.get_nowait()
- if isinstance(value, float):
- self.progress.set(value)
- elif value == "done":
- self.start_btn.configure(state="normal")
- messagebox.showinfo("✅ Done", "Images sorted.")
- return
- except queue.Empty:
- pass
- self.root.after(100, self.check_queue)
- def get_exif_parameters(self, filepath):
- try:
- result = subprocess.run(
- ['exiftool', '-Parameters', filepath],
- stdout=subprocess.PIPE,
- stderr=subprocess.DEVNULL,
- text=True,
- encoding='utf-8', # Force UTF-8 decoding
- check=True
- )
- if result.stdout:
- for line in result.stdout.splitlines():
- if line.startswith("Parameters"):
- text = line.split(":", 1)[1].strip().lower()
- if "negative prompt" in text:
- return text.split("negative prompt")[0].strip()
- return text
- except Exception as e:
- print(f"[EXIF Error]: {e}")
- return ""
- def collect_files(self, folder, include_sub):
- image_exts = ('.png', '.jpg', '.jpeg', '.webp', '.bmp')
- video_exts = ('.mp4', '.webm', '.mov', '.mkv')
- allowed_exts = image_exts + video_exts
- files = []
- if include_sub:
- for root, _, filenames in os.walk(folder):
- for f in filenames:
- if f.lower().endswith(allowed_exts):
- files.append(os.path.join(root, f))
- else:
- # Only look in the top-level folder
- for f in os.listdir(folder):
- full_path = os.path.join(folder, f)
- if os.path.isfile(full_path) and f.lower().endswith(allowed_exts):
- files.append(full_path)
- return files
- def safe_move(self, src, dst):
- try:
- if os.path.exists(src):
- shutil.move(src, dst)
- except Exception as e:
- print(f"[Move Error]: {src} -> {dst} | {e}")
- def sort_thread(self, input_folders, output_folder, keyword_map, include_sub):
- try:
- all_files = []
- for folder in input_folders:
- all_files.extend(self.collect_files(folder, include_sub))
- total = len(all_files)
- if total == 0:
- self.queue.put("done")
- return
- for i, filepath in enumerate(all_files, start=1):
- filename = os.path.basename(filepath)
- ext = os.path.splitext(filename)[1].lower()
- # Video file
- if ext in [".mp4", ".webm", ".mov", ".mkv"]:
- target = os.path.join(output_folder, "videos", filename)
- os.makedirs(os.path.dirname(target), exist_ok=True)
- self.safe_move(filepath, target)
- self.queue.put(i / total)
- continue
- # Get EXIF
- exif = self.get_exif_parameters(filepath)
- if not exif:
- target = os.path.join(output_folder, "unsorted", "noexif", filename)
- os.makedirs(os.path.dirname(target), exist_ok=True)
- self.safe_move(filepath, target)
- self.queue.put(i / total)
- continue
- exif = exif.lower()
- matched = False
- for kw, data in keyword_map.items():
- if any(term in exif for term in data["match"]):
- # Check subkeywords
- for sub in data["subs"]:
- if any(sub_term in exif for sub_term in sub["match"]):
- target = os.path.join(output_folder, kw.replace(" ", "_"), sub["main"].replace(" ", "_"), filename)
- os.makedirs(os.path.dirname(target), exist_ok=True)
- self.safe_move(filepath, target)
- matched = True
- break
- if not matched:
- # No sub match, just keyword
- target = os.path.join(output_folder, kw.replace(" ", "_"), filename)
- os.makedirs(os.path.dirname(target), exist_ok=True)
- self.safe_move(filepath, target)
- matched = True
- break
- if not matched:
- target = os.path.join(output_folder, "unsorted", filename)
- os.makedirs(os.path.dirname(target), exist_ok=True)
- self.safe_move(filepath, target)
- self.queue.put(i / total)
- except Exception as e:
- print(f"[Thread Error]: {e}")
- self.queue.put("done")
- if __name__ == "__main__":
- ctk.set_appearance_mode("System")
- ctk.set_default_color_theme("blue")
- app = ctk.CTk()
- ImageSorterApp(app)
- app.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment