302 lines
11 KiB
Python
302 lines
11 KiB
Python
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()
|