import os import json import threading import subprocess import tkinter as tk from tkinter import filedialog, messagebox BASE_DIR = os.path.dirname(os.path.abspath(__file__)) CONFIG_FILE = os.path.join(BASE_DIR, "config.json") class DirectorySelectorApp: def __init__(self, root): self.root = root self.root.title("Selector de Directorios ES-DE / ROMs") self.root.geometry("700x650") # Variables de rutas self.path_esde_src = tk.StringVar() self.path_roms_src = tk.StringVar() self.path_esde_dst = tk.StringVar() self.path_roms_dst = tk.StringVar() # --------------------------- # RUTAS DE ORIGEN # --------------------------- tk.Label(root, text="Rutas de origen", font=("Arial", 12, "bold")).pack(pady=(10, 0)) self.create_path_selector("Origen ES-DE:", self.path_esde_src) self.create_path_selector("Origen ROMs:", self.path_roms_src, callback=self.update_directory_list) # --------------------------- # LISTA DE DIRECTORIOS # --------------------------- tk.Label(root, text="Directorios encontrados en ROMs:", font=("Arial", 11)).pack(pady=(15, 5)) self.listbox = tk.Listbox(root, selectmode=tk.MULTIPLE, height=15) self.listbox.pack(fill="both", expand=True, padx=10) # --------------------------- # RUTAS DE DESTINO # --------------------------- tk.Label(root, text="Rutas de destino", font=("Arial", 12, "bold")).pack(pady=(15, 0)) self.create_path_selector("Destino ES-DE:", self.path_esde_dst) self.create_path_selector("Destino ROMs:", self.path_roms_dst) # --------------------------- # BOTÓN ROBOCOPY # --------------------------- tk.Button(root, text="Robocopy", font=("Arial", 12, "bold"), command=self.run_robocopy).pack(pady=10) # --------------------------- # PANEL DE LOG # --------------------------- tk.Label(root, text="Progreso:", font=("Arial", 11)).pack(pady=(5, 0)) self.log = tk.Text(root, height=10, state="disabled", bg="#111", fg="#0f0") self.log.pack(fill="both", expand=False, padx=10, pady=5) # Evento de cierre self.root.protocol("WM_DELETE_WINDOW", self.on_close) # Cargar configuración previa self.load_config() # --------------------------------------------------------- # CREAR SELECTOR DE RUTA # --------------------------------------------------------- def create_path_selector(self, label_text, var, callback=None): frame = tk.Frame(self.root) frame.pack(fill="x", padx=10, pady=5) tk.Label(frame, text=label_text, width=15, anchor="w").pack(side="left") tk.Entry(frame, textvariable=var, width=50).pack(side="left", padx=5) tk.Button(frame, text="Buscar", command=lambda: self.choose_path(var, callback)).pack(side="left") def choose_path(self, var, callback): path = filedialog.askdirectory() if not path: return var.set(path) if callback: callback(path) # --------------------------------------------------------- # LISTA DE DIRECTORIOS # --------------------------------------------------------- def update_directory_list(self, path): self.listbox.delete(0, tk.END) if not os.path.isdir(path): return try: dirs = sorted( d for d in os.listdir(path) if os.path.isdir(os.path.join(path, d)) ) for d in dirs: self.listbox.insert(tk.END, d) except Exception as e: messagebox.showerror("Error", f"No se pudo leer la ruta:\n{e}") # --------------------------------------------------------- # LOG # --------------------------------------------------------- def append_log(self, text): self.log.configure(state="normal") self.log.insert(tk.END, text + "\n") self.log.see(tk.END) self.log.configure(state="disabled") # --------------------------------------------------------- # EJECUTAR ROBOCOPY (HILO) # --------------------------------------------------------- def run_robocopy(self): thread = threading.Thread(target=self._run_robocopy_thread) thread.daemon = True thread.start() def _run_robocopy_thread(self): selected = [self.listbox.get(i) for i in self.listbox.curselection()] if not selected: self.append_log("❌ No hay sistemas seleccionados.") return esde_src = self.path_esde_src.get() roms_src = self.path_roms_src.get() esde_dst = self.path_esde_dst.get() roms_dst = self.path_roms_dst.get() if not all([esde_src, roms_src, esde_dst, roms_dst]): self.append_log("❌ ERROR: Debes configurar todas las rutas antes de continuar.") return self.append_log("=" * 60) self.append_log("🚀 INICIANDO PROCESO DE COPIA") self.append_log(f"📦 Total de sistemas a procesar: {len(selected)}") self.append_log("=" * 60) for idx, system in enumerate(selected, 1): self.append_log(f"\n{'=' * 60}") self.append_log(f"🎮 SISTEMA [{idx}/{len(selected)}]: {system.upper()}") self.append_log("=" * 60) # ROMs self.append_log(f"\n📁 [1/3] Copiando ROMs...") self.launch_robocopy_with_log( os.path.join(roms_src, system), os.path.join(roms_dst, system) ) # ES-DE gamelists self.append_log(f"\n📋 [2/3] Copiando gamelists...") self.launch_robocopy_with_log( os.path.join(esde_src, "gamelists", system), os.path.join(esde_dst, "gamelists", system) ) # ES-DE downloaded_media self.append_log(f"\n🖼️ [3/3] Copiando media...") self.launch_robocopy_with_log( os.path.join(esde_src, "downloaded_media", system), os.path.join(esde_dst, "downloaded_media", system) ) self.append_log(f"\n✅ Sistema '{system}' completado") self.append_log("\n" + "=" * 60) self.append_log("🎉 PROCESO COMPLETADO EXITOSAMENTE") self.append_log("=" * 60) def launch_robocopy_with_log(self, src, dst): if not os.path.isdir(src): self.append_log(f" ⚠️ Carpeta no existe (omitido): {os.path.basename(src)}") return os.makedirs(dst, exist_ok=True) cmd = ["robocopy", src, dst, "/MIR", "/NP", "/NDL", "/NFL"] process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, universal_newlines=True ) # Variables para extraer el resumen files_copied = 0 dirs_copied = 0 total_bytes = 0 for line in process.stdout: line = line.strip() # Extraer información relevante del output de robocopy if "Files :" in line and "Copied" in line: parts = line.split() try: copied_idx = parts.index("Copied") if copied_idx + 1 < len(parts): files_copied = int(parts[copied_idx + 1]) except (ValueError, IndexError): pass elif "Dirs :" in line and "Copied" in line: parts = line.split() try: copied_idx = parts.index("Copied") if copied_idx + 1 < len(parts): dirs_copied = int(parts[copied_idx + 1]) except (ValueError, IndexError): pass elif "Bytes :" in line and "Copied" in line: parts = line.split() try: copied_idx = parts.index("Copied") if copied_idx + 1 < len(parts): bytes_str = parts[copied_idx + 1].replace(",", "") total_bytes = int(bytes_str) except (ValueError, IndexError): pass process.wait() # Convertir bytes a formato legible if total_bytes > 0: if total_bytes < 1024: size_str = f"{total_bytes} B" elif total_bytes < 1024**2: size_str = f"{total_bytes/1024:.2f} KB" elif total_bytes < 1024**3: size_str = f"{total_bytes/(1024**2):.2f} MB" else: size_str = f"{total_bytes/(1024**3):.2f} GB" self.append_log(f" ✓ {files_copied} archivos, {dirs_copied} carpetas ({size_str})") else: self.append_log(f" ✓ Sin cambios (ya sincronizado)") # --------------------------------------------------------- # PERSISTENCIA # --------------------------------------------------------- def save_config(self): data = { "esde_src": self.path_esde_src.get(), "roms_src": self.path_roms_src.get(), "esde_dst": self.path_esde_dst.get(), "roms_dst": self.path_roms_dst.get(), "selected": [self.listbox.get(i) for i in self.listbox.curselection()] } try: with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(data, f, indent=4) except Exception as e: messagebox.showerror("Error", f"No se pudo guardar la configuración:\n{e}") def load_config(self): if not os.path.exists(CONFIG_FILE): return try: with open(CONFIG_FILE, "r", encoding="utf-8") as f: data = json.load(f) except Exception: return self.path_esde_src.set(data.get("esde_src", "")) self.path_roms_src.set(data.get("roms_src", "")) self.path_esde_dst.set(data.get("esde_dst", "")) self.path_roms_dst.set(data.get("roms_dst", "")) roms_path = self.path_roms_src.get() if os.path.isdir(roms_path): self.update_directory_list(roms_path) selected = set(data.get("selected", [])) for i in range(self.listbox.size()): if self.listbox.get(i) in selected: self.listbox.selection_set(i) # --------------------------------------------------------- # CIERRE # --------------------------------------------------------- def on_close(self): self.save_config() self.root.destroy() if __name__ == "__main__": root = tk.Tk() app = DirectorySelectorApp(root) root.mainloop()