import os
import shutil
import xml.etree.ElementTree as ET
import tkinter as tk
from tkinter import messagebox, filedialog
from PIL import Image, ImageTk
import subprocess
from tkinter import ttk
class LaunchBoxManager:
def __init__(self, root):
self.root = root
self.root.title("LaunchBox Media Manager")
self.root.geometry("800x600")
self.platforms = {}
self.launchbox_path = None
self.filters = ["Box - Front", "Clear Logo"]
self.show_images = False
self.image_references = {}
self.last_sorted_column = None
self.videos_enabled = False
self.manual_enabled = False
self.sort_ascending = True
self.cell_size_text = (100, 50)
self.cell_size_image = (150, 150)
self.columns = 0
self.image_cache_ratio = "150x150"
self.alternative_path = None
self.max_games_per_page_imagemode = 100
self.make_cache = True
self.current_page = 0
self.total_pages = 0
max_retries = 2
retries = 0
while retries < max_retries:
if self.load_launchbox_path():
break
else:
print("Error: No se pudo cargar la configuración de LaunchBox. Reintentando...")
retries += 1
if retries == max_retries:
print("Error: No se pudo cargar la configuración de LaunchBox después de varios intentos.")
self.root.destroy()
self.setup_ui()
def setup_ui(self):
# Frame superior (para la selección de plataformas)
self.top_frame = tk.Frame(self.root)
self.top_frame.pack(fill="x", padx=10, pady=10)
# Etiqueta y Combobox para seleccionar la plataforma
self.platform_label = tk.Label(self.top_frame, text="Plataforma:")
self.platform_label.pack(side="left", padx=(0, 5))
self.platform_combobox = tk.StringVar()
self.platform_menu = ttk.Combobox(self.top_frame, textvariable=self.platform_combobox, state="readonly")
self.platform_menu.pack(side="left", fill="x", expand=True, padx=(0, 10))
self.platform_combobox.trace("w", self.on_platform_select)
# Botón para cargar plataformas
self.load_button = tk.Button(self.top_frame, text="Cargar Plataformas", command=self.load_platforms)
self.load_button.pack(side="left")
# Frame para los Checkbuttons (Modo Imágenes y Ocultar todo SI)
self.checkbutton_frame = tk.Frame(self.top_frame)
self.checkbutton_frame.pack(side="left", padx=(10, 0))
# Checkbutton para el modo imágenes
self.switch_var = tk.BooleanVar()
self.switch = tk.Checkbutton(self.checkbutton_frame, text="Modo Imágenes", variable=self.switch_var,
command=self.toggle_view_mode)
self.switch.pack(anchor="w") # Alineado a la izquierda dentro del Frame
# Checkbutton para ocultar juegos con "SI" en todas las celdas
self.hide_all_si_var = tk.BooleanVar(value=False) # Desactivado por defecto
self.hide_all_si_check = tk.Checkbutton(self.checkbutton_frame, text="Ocultar todo SI",
variable=self.hide_all_si_var,
command=self.toggle_hide_all_si)
self.hide_all_si_check.pack(anchor="w") # Alineado a la izquierda dentro del Frame
# Botón para abrir la configuración (a la derecha)
self.settings_button = tk.Button(self.top_frame, text="Configuración", command=self.open_settings)
self.settings_button.pack(side="right", padx=(10, 0))
# Frame para los filtros y la tabla
self.filter_frame = tk.LabelFrame(self.root, text="Estado de imágenes")
self.filter_frame.pack(fill="both", expand=True, padx=10, pady=10)
# Canvas y scrollbars para la tabla
self.header_canvas = tk.Canvas(self.filter_frame, borderwidth=1, relief="solid", height=self.cell_size_text[1])
self.header_canvas.pack(fill="x", side="top")
self.header_scrollbar_x = tk.Scrollbar(self.filter_frame, orient="horizontal",
command=lambda *args: self.sync_scroll(*args))
self.header_scrollbar_x.pack(side="top", fill="x")
self.header_canvas.config(xscrollcommand=self.header_scrollbar_x.set)
self.canvas_frame = tk.Frame(self.filter_frame)
self.canvas_frame.pack(fill="both", expand=True)
self.canvas = tk.Canvas(self.canvas_frame, borderwidth=1, relief="solid")
self.canvas.pack(side="left", fill="both", expand=True)
self.canvas_scrollbar_y = tk.Scrollbar(self.canvas_frame, orient="vertical", command=self.canvas.yview)
self.canvas_scrollbar_y.pack(side="right", fill="y")
self.canvas.config(yscrollcommand=self.canvas_scrollbar_y.set)
self.canvas_scrollbar_x = tk.Scrollbar(self.filter_frame, orient="horizontal",
command=lambda *args: self.sync_scroll(*args))
self.canvas_scrollbar_x.pack(side="bottom", fill="x")
self.canvas.config(xscrollcommand=self.canvas_scrollbar_x.set)
self.canvas.bind("<Configure>", lambda e: self.sync_scroll())
# Vincular el evento de la rueda del ratón al Canvas
self.canvas.bind("<MouseWheel>", self.on_mouse_wheel_cells)
# Frame para la paginación y botones adicionales
self.pagination_frame = tk.Frame(self.root)
self.pagination_frame.pack(fill="x", padx=10, pady=10)
self.center_frame = tk.Frame(self.pagination_frame)
self.center_frame.pack()
# Botón "Anterior"
self.prev_button = tk.Button(self.center_frame, text="Anterior", command=self.prev_page)
self.prev_button.pack(side="left")
# Etiqueta de la página actual
self.page_label = tk.Label(self.center_frame, text="Página 1 de 1")
self.page_label.pack(side="left", padx=10)
# Botón "Siguiente"
self.next_button = tk.Button(self.center_frame, text="Siguiente", command=self.next_page)
self.next_button.pack(side="left")
# Botón "Generar todo Cache"
self.generate_all_cache_button = tk.Button(self.center_frame, text="Generar todo Cache",
command=self.generate_all_cache)
self.generate_all_cache_button.pack(side="left", padx=(10, 0))
# Botón "Regenerar caché"
self.regenerate_cache_button = tk.Button(self.center_frame, text="Regenerar caché",
command=self.regenerate_cache)
self.regenerate_cache_button.pack(side="left", padx=(10, 0))
# Ocultar o mostrar los botones de caché según make_cache
self.update_cache_buttons_visibility()
def on_mouse_wheel_cells(self, event):
menu_state = str(self.platform_menu.cget("state")) # Guardarlo como string inmutable
if menu_state == "readonly":
delta = -event.delta
if delta > 0:
self.canvas.yview_scroll(1, "units")
else:
self.canvas.yview_scroll(-1, "units")
def on_mouse_wheel_platforms(self, event):
"""
Maneja el desplazamiento vertical con la rueda del ratón para la lista de plataformas.
"""
menu_state = self.platform_menu.cget("state")
if menu_state == "normal": # Solo desplazar si el menú está desplegado
current_index = self.platform_menu.current()
if event.delta > 0:
new_index = max(0, current_index - 1) # Desplazar hacia arriba
else:
new_index = min(len(self.platform_menu["values"]) - 1, current_index + 1) # Desplazar hacia abajo
self.platform_menu.current(new_index)
self.platform_combobox.set(self.platform_menu["values"][new_index])
self.on_platform_select()
def sync_scroll(self, *args):
"""
Sincroniza el desplazamiento horizontal entre el header_canvas y el canvas principal.
No afecta el desplazamiento vertical.
"""
if len(args) == 2 and args[0] == "moveto":
fraction = float(args[1])
self.header_canvas.xview_moveto(fraction)
self.canvas.xview_moveto(fraction)
elif len(args) == 2 and args[0] == "scroll":
amount, units = args[1].split()
self.header_canvas.xview_scroll(amount, units)
self.canvas.xview_scroll(amount, units)
else:
fraction = self.canvas.xview()[0]
self.header_canvas.xview_moveto(fraction)
def toggle_hide_all_si(self):
# Si el Checkbutton está activado, ocultamos los juegos con "SI" en todas las celdas
if self.hide_all_si_var.get():
# Ordenar los juegos alfabéticamente antes de aplicar el filtro
selected_platform = self.platform_combobox.get()
if not selected_platform:
return
xml_path = self.platforms[selected_platform]
tree = ET.parse(xml_path)
root = tree.getroot()
games = root.findall("Game")
game_dict = {}
for game in games:
title_element = game.find("Title")
title = title_element.text if title_element is not None else None
app_path_element = game.find("ApplicationPath")
if app_path_element is not None and app_path_element.text:
app_filename = os.path.basename(app_path_element.text)
app_name = os.path.splitext(app_filename)[0]
else:
app_name = None
if title:
game_dict[title] = [title]
if app_name and app_name != title:
game_dict[title].append(app_name)
# Ordenar los juegos alfabéticamente (de la A a la Z)
sorted_game_dict = dict(
sorted(game_dict.items(), key=lambda item: (self.natural_sort_key(item[0]), item[0])))
# Aplicar el filtro "Ocultar todo SI" sobre la lista ordenada
self.hide_all_si(sorted_game_dict)
else:
# Si está desactivado, mostramos todos los juegos nuevamente
self.on_platform_select()
def update_cache_buttons_visibility(self):
"""Actualiza la visibilidad de los botones de caché según el valor de make_cache."""
if self.make_cache:
self.generate_all_cache_button.pack(side="left", padx=(10, 0))
self.regenerate_cache_button.pack(side="left", padx=(10, 0))
else:
self.generate_all_cache_button.pack_forget()
self.regenerate_cache_button.pack_forget()
def load_platforms(self):
self.platform_combobox.set("") # Limpiar la selección actual
self.platforms.clear() # Limpiar el diccionario de plataformas
# Construir la ruta a la carpeta de plataformas
platforms_path = os.path.join(self.launchbox_path, "Data", "Platforms").replace("\\", "/")
if not os.path.exists(platforms_path):
messagebox.showerror("Error", f"No se encontró la carpeta de plataformas en: {platforms_path}")
return
# Obtener los nombres de las plataformas
platform_names = []
for filename in os.listdir(platforms_path):
if filename.endswith(".xml"):
platform_name = filename[:-4] # Eliminar la extensión .xml
self.platforms[platform_name] = os.path.join(platforms_path, filename).replace("\\", "/")
platform_names.append(platform_name)
if not platform_names:
messagebox.showerror("Error", "No se encontraron archivos XML en la carpeta de plataformas.")
return
# Asignar los valores al Combobox
self.platform_menu["values"] = platform_names
self.platform_menu.current(0) # Seleccionar la primera plataforma por defecto
# Forzar la actualización de la interfaz
self.platform_menu.update_idletasks()
# Cargar los datos de la plataforma seleccionada
self.on_platform_select()
def hide_all_si(self, game_dict=None):
if game_dict is None:
selected_platform = self.platform_combobox.get()
if not selected_platform:
return
xml_path = self.platforms[selected_platform]
tree = ET.parse(xml_path)
root = tree.getroot()
games = root.findall("Game")
game_dict = {}
for game in games:
title_element = game.find("Title")
title = title_element.text if title_element is not None else None
app_path_element = game.find("ApplicationPath")
if app_path_element is not None and app_path_element.text:
app_filename = os.path.basename(app_path_element.text)
app_name = os.path.splitext(app_filename)[0]
else:
app_name = None
if title:
game_dict[title] = [title]
if app_name and app_name != title:
game_dict[title].append(app_name)
# Filtrar juegos que tienen "SI" en todas las celdas
filtered_game_dict = self.filter_all_si(game_dict)
# Actualizar la tabla con los juegos filtrados
selected_platform = self.platform_combobox.get()
self.load_filter_file(selected_platform, filtered_game_dict)
def on_platform_select(self, *args):
selected_platform = self.platform_combobox.get()
if not selected_platform:
return
try:
xml_path = self.platforms[selected_platform]
tree = ET.parse(xml_path)
root = tree.getroot()
if root.tag != "LaunchBox":
messagebox.showerror("Error", f"El archivo XML no tiene la estructura esperada. Elemento raíz: {root.tag}")
return
games = root.findall("Game")
game_dict = {}
for game in games:
title_element = game.find("Title")
title = title_element.text if title_element is not None else None
app_path_element = game.find("ApplicationPath")
if app_path_element is not None and app_path_element.text:
app_filename = os.path.basename(app_path_element.text)
app_name = os.path.splitext(app_filename)[0]
else:
app_name = None
if title:
game_dict[title] = [title]
if app_name and app_name != title:
game_dict[title].append(app_name)
sorted_game_dict = dict(sorted(game_dict.items(), key=lambda item: (self.natural_sort_key(item[0]), item[0])))
self.load_filter_file(selected_platform, sorted_game_dict)
except Exception as e:
messagebox.showerror("Error", f"No se pudo leer el archivo XML: {e}")
def generate_all_cache(self):
selected_platform = self.platform_combobox.get()
if not selected_platform:
return
self.progress_label = tk.Label(self.root, text="Generando Caché...")
self.progress_label.place(relx=0.5, rely=0.5, anchor="center")
self.root.update()
original_page = self.current_page
for page in range(self.total_pages):
self.current_page = page
self.on_platform_select()
self.progress_label.config(text=f"Generando página {page + 1} de {self.total_pages}")
self.root.update()
self.current_page = original_page
self.on_platform_select()
self.progress_label.destroy()
def open_settings(self):
current_dir = os.path.dirname(os.path.abspath(__file__))
settings_path = os.path.join(current_dir, "Settings.py")
subprocess.run(["python", settings_path], check=True)
# Volver a cargar la configuración después de guardar los cambios
self.load_launchbox_path()
# Actualizar la visibilidad de los botones de caché
self.update_cache_buttons_visibility()
# Actualizar otros elementos de la interfaz si es necesario
self.on_platform_select() # Esto actualiza la tabla con los nuevos valores
def load_launchbox_path(self):
config_file = os.path.join(os.path.dirname(__file__), "config.txt").replace("\\", "/")
if not os.path.exists(config_file):
with open(config_file, "w") as file:
file.write("path=\n")
file.write("filters=\n")
file.write("image_cache_ratio=150x150\n")
file.write("alternative_path=\n")
file.write("image_cell=150x150\n")
file.write("text_cell=150x50\n")
file.write("max_games_per_page_imagemode=100\n")
file.write("color_media_yes_title=#0073E6\n")
file.write("color_media_yes_rom=#00E673\n")
file.write("color_media_both=#E67300\n")
file.write("color_media_no=#E60073\n")
file.write("color_no_trans=#C0C0C0\n")
file.write("make_cache=true\n")
print("Archivo config.txt creado con la estructura básica.")
return False
with open(config_file, "r") as file:
config_lines = file.readlines()
for line in config_lines:
if line.startswith("path="):
self.launchbox_path = line.strip().split('=')[1]
if line.startswith("filters="):
filters_line = line.strip().split('=')[1]
self.filters = [f.strip().strip('"') for f in filters_line.split(',')]
if line.startswith("image_cache_ratio="):
self.image_cache_ratio = line.strip().split('=')[1]
if line.startswith("alternative_path="):
self.alternative_path = line.strip().split('=')[1]
if line.startswith("image_cell="):
try:
width, height = line.strip().split('=')[1].split('x')
self.cell_size_image = (int(width), int(height))
except ValueError:
print(
"Error: El valor de image_cell no tiene el formato correcto. Usando valores predeterminados (200x200).")
self.cell_size_image = (200, 200)
if line.startswith("text_cell="):
try:
width, height = line.strip().split('=')[1].split('x')
self.cell_size_text = (int(width), int(height))
except ValueError:
print(
"Error: El valor de text_cell no tiene el formato correcto. Usando valores predeterminados (100x50).")
self.cell_size_text = (100, 50)
if line.startswith("max_games_per_page_imagemode="):
try:
self.max_games_per_page_imagemode = int(line.strip().split('=')[1])
except ValueError:
print(
"Error: El valor de max_games_per_page_imagemode no es un número válido. Usando valor predeterminado (20).")
self.max_games_per_page_imagemode = 20
if line.startswith("color_media_yes_title="):
self.color_media_yes_title = line.strip().split('=')[1]
if line.startswith("color_media_yes_rom="):
self.color_media_yes_rom = line.strip().split('=')[1]
if line.startswith("color_media_both="):
self.color_media_both = line.strip().split('=')[1]
if line.startswith("color_media_no="):
self.color_media_no = line.strip().split('=')[1]
if line.startswith("color_no_trans="):
self.color_no_trans = line.strip().split('=')[1]
if line.startswith("color_media_both="):
self.color_media_both = line.strip().split('=')[1]
if line.startswith("make_cache="): # Nuevo parámetro
self.make_cache = line.strip().split('=')[1].lower() == "true"
return True
def load_platforms(self):
self.platform_combobox.set("") # Limpiar la selección actual
self.platforms.clear() # Limpiar el diccionario de plataformas
# Construir la ruta a la carpeta de plataformas
platforms_path = os.path.join(self.launchbox_path, "Data", "Platforms").replace("\\", "/")
if not os.path.exists(platforms_path):
messagebox.showerror("Error", f"No se encontró la carpeta de plataformas en: {platforms_path}")
return
# Obtener los nombres de las plataformas
platform_names = []
for filename in os.listdir(platforms_path):
if filename.endswith(".xml"):
platform_name = filename[:-4] # Eliminar la extensión .xml
self.platforms[platform_name] = os.path.join(platforms_path, filename).replace("\\", "/")
platform_names.append(platform_name)
if not platform_names:
messagebox.showerror("Error", "No se encontraron archivos XML en la carpeta de plataformas.")
return
# Asignar los valores al Combobox
self.platform_menu["values"] = platform_names
self.platform_menu.current(0) # Seleccionar la primera plataforma por defecto
# Forzar la actualización de la interfaz
self.platform_menu.update_idletasks()
# Cargar los datos de la plataforma seleccionada
self.on_platform_select()
def on_platform_select(self, *args):
selected_platform = self.platform_combobox.get()
if not selected_platform:
return
try:
xml_path = self.platforms[selected_platform]
tree = ET.parse(xml_path)
root = tree.getroot()
if root.tag != "LaunchBox":
messagebox.showerror("Error", f"El archivo XML no tiene la estructura esperada. Elemento raíz: {root.tag}")
return
games = root.findall("Game")
game_dict = {}
for game in games:
title_element = game.find("Title")
title = title_element.text if title_element is not None else None
app_path_element = game.find("ApplicationPath")
if app_path_element is not None and app_path_element.text:
app_filename = os.path.basename(app_path_element.text)
app_name = os.path.splitext(app_filename)[0]
else:
app_name = None
if title:
game_dict[title] = [title]
if app_name and app_name != title:
game_dict[title].append(app_name)
sorted_game_dict = dict(sorted(game_dict.items(), key=lambda item: (self.natural_sort_key(item[0]), item[0])))
self.load_filter_file(selected_platform, sorted_game_dict)
except Exception as e:
messagebox.showerror("Error", f"No se pudo leer el archivo XML: {e}")
def load_filter_file(self, platform, game_dict):
filters_to_use = self.filters[:]
if self.videos_enabled:
filters_to_use.append("Videos")
if self.manual_enabled:
filters_to_use.append("Manual")
self.columns = len(filters_to_use) + 1 # Definir self.columns
self.total_pages = (len(game_dict) + self.max_games_per_page_imagemode - 1) // self.max_games_per_page_imagemode
self.update_pagination_controls()
start_index = self.current_page * self.max_games_per_page_imagemode
end_index = start_index + self.max_games_per_page_imagemode
games_to_show = list(game_dict.items())[start_index:end_index]
self.canvas.delete("all")
self.image_references = {}
self.draw_table(games_to_show, filters_to_use)
total_width = sum(
self.cell_size_image[0] if self.show_images else self.cell_size_text[0] for _ in range(self.columns))
total_height = self.rows * (self.cell_size_image[1] if self.show_images else self.cell_size_text[1])
self.canvas.config(scrollregion=(0, 0, total_width, total_height))
def prev_page(self):
if self.current_page > 0:
self.current_page -= 1
self.update_pagination_controls()
self.on_platform_select()
def next_page(self):
if self.current_page < self.total_pages - 1:
self.current_page += 1
self.update_pagination_controls()
self.on_platform_select()
def update_pagination_controls(self):
self.page_label.config(text=f"Página {self.current_page + 1} de {self.total_pages}")
self.prev_button.config(state="normal" if self.current_page > 0 else "disabled")
self.next_button.config(state="normal" if self.current_page < self.total_pages - 1 else "disabled")
def toggle_view_mode(self):
self.show_images = self.switch_var.get()
if self.show_images:
self.header_canvas.config(height=self.cell_size_image[1])
else:
self.header_canvas.config(height=self.cell_size_text[1])
self.on_platform_select()
def search_image_in_folder(self, folder, search_names, extensions):
"""
Busca imágenes en una carpeta y sus subcarpetas de manera recursiva.
Retorna la ruta de la imagen si la encuentra, o None si no la encuentra.
"""
for root, dirs, files in os.walk(folder):
for file in files:
# Verificar si el archivo coincide con alguno de los nombres y extensiones
for search_name in search_names:
for ext in extensions:
if file.startswith(search_name) and file.endswith(ext):
return os.path.join(root, file), search_name
return None, None
def draw_table(self, games_to_show, filters):
self.rows = len(games_to_show)
if self.show_images:
cell_width, cell_height = self.cell_size_image
else:
cell_width, cell_height = self.cell_size_text
total_width = sum(cell_width for _ in range(self.columns))
self.header_canvas.config(width=total_width, height=cell_height)
self.header_canvas.delete("all")
for col, filter_name in enumerate(["Título"] + filters):
self.draw_cell(0, col, filter_name, header=True, canvas=self.header_canvas)
self.header_canvas.tag_bind(f"header_{col}", "<Button-1>", lambda event, col=col: self.on_header_click(col))
for row, (title, search_names) in enumerate(games_to_show, start=0):
self.draw_cell(row, 0, title)
for col, filter_name in enumerate(filters, start=1):
if filter_name == "Videos":
# Definir las rutas principales y alternativas
main_folder_path = os.path.join(self.launchbox_path, "Videos",
self.platform_combobox.get()).replace("\\", "/")
alternative_folder_path = os.path.join(self.alternative_path, self.platform_combobox.get(),
"Videos").replace("\\",
"/") if self.alternative_path else None
file_found = False
match_type = None
file_path = None
# Buscar en la ruta alternativa primero (si existe)
if alternative_folder_path and os.path.exists(alternative_folder_path):
# Buscar <Title>-01.mp4
file_path = os.path.join(alternative_folder_path, f"{search_names[0]}-01.mp4").replace("\\",
"/")
if os.path.isfile(file_path):
file_found = True
match_type = 'title'
else:
# Buscar <ApplicationPath>.mp4 (sin -01)
if len(search_names) > 1:
file_path = os.path.join(alternative_folder_path, f"{search_names[1]}.mp4").replace(
"\\", "/")
if os.path.isfile(file_path):
file_found = True
match_type = 'rom'
else:
# Si <Title> y <ApplicationPath> son iguales, buscar <Title>.mp4 (sin -01)
file_path = os.path.join(alternative_folder_path, f"{search_names[0]}.mp4").replace(
"\\", "/")
if os.path.isfile(file_path):
file_found = True
match_type = 'rom'
# Si no se encontró en la ruta alternativa, buscar en la ruta principal
if not file_found and os.path.exists(main_folder_path):
# Buscar <Title>-01.mp4
file_path = os.path.join(main_folder_path, f"{search_names[0]}-01.mp4").replace("\\", "/")
if os.path.isfile(file_path):
file_found = True
match_type = 'title'
else:
# Buscar <ApplicationPath>.mp4 (sin -01)
if len(search_names) > 1:
file_path = os.path.join(main_folder_path, f"{search_names[1]}.mp4").replace("\\", "/")
if os.path.isfile(file_path):
file_found = True
match_type = 'rom'
else:
# Si <Title> y <ApplicationPath> son iguales, buscar <Title>.mp4 (sin -01)
file_path = os.path.join(main_folder_path, f"{search_names[0]}.mp4").replace("\\", "/")
if os.path.isfile(file_path):
file_found = True
match_type = 'rom'
# Mostrar el resultado en la tabla
if self.show_images:
if file_found:
self.draw_cell(row, col, "SI",
cell_color=self.color_media_yes_title if match_type == 'title' else self.color_media_yes_rom)
else:
self.draw_cell(row, col, "NO", cell_color=self.color_media_no)
else:
cell_value = "SI (Title)" if match_type == 'title' else "SI (Rom)" if match_type == 'rom' else "NO"
cell_color = self.color_media_yes_title if match_type == 'title' else self.color_media_yes_rom if match_type == 'rom' else self.color_media_no
self.draw_cell(row, col, cell_value, cell_color=cell_color, is_text=True)
elif filter_name == "Manual":
# Definir las rutas principales y alternativas
main_folder_path = os.path.join(self.launchbox_path, "Manuals",
self.platform_combobox.get()).replace("\\", "/")
alternative_folder_path = os.path.join(self.alternative_path, self.platform_combobox.get(),
"Manuals").replace("\\",
"/") if self.alternative_path else None
file_found = False
match_type = None
file_path = None
# Buscar en la ruta alternativa primero (si existe)
if alternative_folder_path and os.path.exists(alternative_folder_path):
# Buscar <Title>-01.pdf
file_path = os.path.join(alternative_folder_path, f"{search_names[0]}-01.pdf").replace("\\",
"/")
if os.path.isfile(file_path):
file_found = True
match_type = 'title'
else:
# Buscar <ApplicationPath>.pdf (sin -01)
if len(search_names) > 1:
file_path = os.path.join(alternative_folder_path, f"{search_names[1]}.pdf").replace(
"\\", "/")
if os.path.isfile(file_path):
file_found = True
match_type = 'rom'
else:
# Si <Title> y <ApplicationPath> son iguales, buscar <Title>.pdf (sin -01)
file_path = os.path.join(alternative_folder_path, f"{search_names[0]}.pdf").replace(
"\\", "/")
if os.path.isfile(file_path):
file_found = True
match_type = 'rom'
# Si no se encontró en la ruta alternativa, buscar en la ruta principal
if not file_found and os.path.exists(main_folder_path):
# Buscar <Title>-01.pdf
file_path = os.path.join(main_folder_path, f"{search_names[0]}-01.pdf").replace("\\", "/")
if os.path.isfile(file_path):
file_found = True
match_type = 'title'
else:
# Buscar <ApplicationPath>.pdf (sin -01)
if len(search_names) > 1:
file_path = os.path.join(main_folder_path, f"{search_names[1]}.pdf").replace("\\", "/")
if os.path.isfile(file_path):
file_found = True
match_type = 'rom'
else:
# Si <Title> y <ApplicationPath> son iguales, buscar <Title>.pdf (sin -01)
file_path = os.path.join(main_folder_path, f"{search_names[0]}.pdf").replace("\\", "/")
if os.path.isfile(file_path):
file_found = True
match_type = 'rom'
# Mostrar el resultado en la tabla
if self.show_images:
if file_found:
self.draw_cell(row, col, "SI",
cell_color=self.color_media_yes_title if match_type == 'title' else self.color_media_yes_rom)
else:
self.draw_cell(row, col, "NO", cell_color=self.color_media_no)
else:
cell_value = "SI (Title)" if match_type == 'title' else "SI (Rom)" if match_type == 'rom' else "NO"
cell_color = self.color_media_yes_title if match_type == 'title' else self.color_media_yes_rom if match_type == 'rom' else self.color_media_no
self.draw_cell(row, col, cell_value, cell_color=cell_color, is_text=True)
else:
image_folder = os.path.join(self.launchbox_path, "Images", self.platform_combobox.get(),
filter_name).replace("\\", "/")
image_found = False
image_path = None
match_type = None
title_found = False
rom_found = False
title_path = None
rom_path = None
if os.path.exists(image_folder):
# Buscar <Title>-01.ext de forma recursiva
for ext in ["png", "jpg"]:
for root, dirs, files in os.walk(image_folder):
for file in files:
if file.startswith(f"{search_names[0]}-01") and file.endswith(ext):
title_path = os.path.join(root, file)
title_found = True
break
if title_found:
break
if title_found:
break
# Buscar <ApplicationPath>.ext de forma no recursiva
if len(search_names) > 1:
for ext in ["png", "jpg"]:
rom_path = os.path.join(image_folder, f"{search_names[1]}.{ext}").replace("\\", "/")
if os.path.isfile(rom_path):
rom_found = True
break
else:
# Si <Title> y <ApplicationPath> son iguales, buscar <Title>.ext (sin -01)
for ext in ["png", "jpg"]:
rom_path = os.path.join(image_folder, f"{search_names[0]}.{ext}").replace("\\", "/")
if os.path.isfile(rom_path):
rom_found = True
break
# Determinar el tipo de coincidencia
if title_found and rom_found:
match_type = 'title/rom'
image_path = title_path # Usar la imagen de Title para mostrar
elif title_found:
match_type = 'title'
image_path = title_path
elif rom_found:
match_type = 'rom'
image_path = rom_path
if self.show_images:
if image_path:
self.draw_image_cell(row, col, image_path, match_type)
else:
self.draw_cell(row, col, "NO", cell_color=self.color_media_no)
else:
if match_type == 'title/rom':
cell_value = "SI (Title/Rom)"
cell_color = self.color_media_both
elif match_type == 'title':
cell_value = "SI (Title)"
cell_color = self.color_media_yes_title
elif match_type == 'rom':
cell_value = "SI (Rom)"
cell_color = self.color_media_yes_rom
else:
cell_value = "NO"
cell_color = self.color_media_no
self.draw_cell(row, col, cell_value, cell_color=cell_color, is_text=True)
total_width = sum(
self.cell_size_image[0] if self.show_images else self.cell_size_text[0] for _ in range(self.columns))
total_height = len(games_to_show) * (self.cell_size_image[1] if self.show_images else self.cell_size_text[1])
self.canvas.config(scrollregion=(0, 0, total_width, total_height))
self.header_canvas.config(scrollregion=(0, 0, total_width, cell_height))
# Forzar el foco al Canvas para que reciba eventos de la rueda del ratón
self.canvas.focus_set()
def on_header_click(self, col):
# Llamar al método para ordenar los datos
self.sort_by_column(col)
def draw_cell(self, row, col, value, header=False, cell_color=None, is_text=False, canvas=None):
if canvas is None:
canvas = self.canvas
if header:
if self.show_images:
cell_width, cell_height = self.cell_size_image
else:
cell_width, cell_height = self.cell_size_text
elif is_text or not self.show_images:
cell_width, cell_height = self.cell_size_text
else:
cell_width, cell_height = self.cell_size_image
x1 = col * cell_width
y1 = row * cell_height
x2 = x1 + cell_width
y2 = y1 + cell_height
if value == "SI (Title)":
fill_color = self.color_media_yes_title
elif value == "SI (Rom)":
fill_color = self.color_media_yes_rom
elif value == "NO":
fill_color = self.color_media_no
else:
fill_color = cell_color or "white"
# Dibujar la celda
canvas.create_rectangle(x1, y1, x2, y2, fill=fill_color, outline="black",
tags=f"header_{col}" if header else f"cell_{row}_{col}")
font_style = ("Arial", 10, "bold") if header else ("Arial", 10)
wrapped_text = self.wrap_text(value, cell_width - 10)
text_y = y1 + (cell_height - len(wrapped_text) * 12) / 2
for line in wrapped_text:
canvas.create_text(x1 + 5, text_y, anchor="w", text=line, font=font_style,
tags=f"header_{col}" if header else f"cell_{row}_{col}")
text_y += 12
def wrap_text(self, text, max_width):
import textwrap
return textwrap.wrap(text, width=max_width // 7)
def draw_image_cell(self, row, col, image_path, match_type):
try:
cell_width, cell_height = self.cell_size_image
if self.make_cache:
# Cargar desde la caché
cache_folder = os.path.join(os.path.dirname(__file__), "cache", self.platform_combobox.get(),
self.filters[col - 1])
os.makedirs(cache_folder, exist_ok=True)
cache_image_path = os.path.join(cache_folder,
os.path.basename(image_path).replace(".png", ".jpg").replace(".PNG",
".jpg"))
width, height = map(int, self.image_cache_ratio.split("x"))
if not os.path.exists(cache_image_path):
try:
file_size = os.path.getsize(image_path)
if file_size > 100 * 1024 * 1024:
raise ValueError(
f"El archivo {image_path} es demasiado grande ({file_size / (1024 * 1024):.2f} MB)")
with Image.open(image_path) as image:
if image.mode in ("P", "1", "L", "LA"):
image = image.convert("RGBA")
if image.mode == "RGBA":
background = Image.new("RGB", image.size, self.color_no_trans)
background.paste(image, mask=image.split()[-1])
image = background
image.thumbnail((width, height))
image.save(cache_image_path, "JPEG")
except Exception as e:
print(f"Error al procesar la imagen {image_path}: {e}")
with open("imagenes_con_error.txt", "a") as error_file:
error_file.write(f"{image_path}\n")
self.draw_cell(row, col, "NO", cell_color=self.color_media_no)
return
with Image.open(cache_image_path) as image:
tk_image = ImageTk.PhotoImage(image)
self.image_references[(row, col)] = tk_image
else:
# Cargar directamente desde la fuente
with Image.open(image_path) as image:
if image.mode in ("P", "1", "L", "LA"):
image = image.convert("RGBA")
if image.mode == "RGBA":
background = Image.new("RGB", image.size, self.color_no_trans)
background.paste(image, mask=image.split()[-1])
image = background
image.thumbnail((cell_width, cell_height))
tk_image = ImageTk.PhotoImage(image)
self.image_references[(row, col)] = tk_image
x1 = col * self.cell_size_image[0]
y1 = row * self.cell_size_image[1]
bg_color = self.color_media_yes_title if match_type == 'title' else self.color_media_yes_rom
self.canvas.create_rectangle(x1, y1, x1 + cell_width, y1 + cell_height, fill=bg_color, outline="black")
self.canvas.create_image(x1 + cell_width // 2, y1 + cell_height // 2, anchor="center", image=tk_image)
except Exception as e:
print(f"Error inesperado al procesar la imagen {image_path}: {e}")
self.draw_cell(row, col, "NO", cell_color=self.color_media_no)
with open("imagenes_con_error.txt", "a") as error_file:
error_file.write(f"{image_path}\n")
def regenerate_cache(self):
selected_platform = self.platform_combobox.get()
if not selected_platform:
return
self.progress_label = tk.Label(self.root, text="Generando Caché...")
self.progress_label.place(relx=0.5, rely=0.5, anchor="center")
self.root.update()
current_platform = self.platform_combobox.get()
cache_folder = os.path.join(os.path.dirname(__file__), "cache", current_platform)
if os.path.exists(cache_folder):
shutil.rmtree(cache_folder)
self.load_platforms()
self.platform_combobox.set(current_platform)
self.on_platform_select()
self.progress_label.destroy()
def natural_sort_key(self, text):
import re
return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', text)]
def sort_by_column(self, col):
selected_platform = self.platform_combobox.get()
if not selected_platform:
return
xml_path = self.platforms[selected_platform]
tree = ET.parse(xml_path)
root = tree.getroot()
games = root.findall("Game")
game_dict = {}
for game in games:
title_element = game.find("Title")
title = title_element.text if title_element is not None else None
app_path_element = game.find("ApplicationPath")
if app_path_element is not None and app_path_element.text:
app_filename = os.path.basename(app_path_element.text)
app_name = os.path.splitext(app_filename)[0]
else:
app_name = None
if title:
game_dict[title] = [title]
if app_name and app_name != title:
game_dict[title].append(app_name)
# Ordenar los juegos
if col == 0:
# Ordenar por título
sorted_game_dict = dict(
sorted(game_dict.items(), key=lambda item: (self.natural_sort_key(item[0]), item[0])))
else:
# Ordenar por la columna seleccionada
filter_name = self.filters[col - 1]
sorted_game_dict = dict(
sorted(game_dict.items(), key=lambda item: self.get_filter_value(item[1], filter_name)))
# Alternar entre orden ascendente y descendente
if self.last_sorted_column == col:
self.sort_ascending = not self.sort_ascending
if not self.sort_ascending:
sorted_game_dict = dict(reversed(list(sorted_game_dict.items())))
else:
self.sort_ascending = True
self.last_sorted_column = col
# Si el filtro "Ocultar todo SI" está activado, aplicar el filtro después de ordenar
if self.hide_all_si_var.get():
sorted_game_dict = self.filter_all_si(sorted_game_dict)
# Actualizar la interfaz gráfica con los juegos ordenados (y filtrados si es necesario)
self.load_filter_file(selected_platform, sorted_game_dict)
def filter_all_si(self, game_dict):
"""
Filtra los juegos que tienen "SI" en todas las celdas.
"""
filtered_game_dict = {}
for title, search_names in game_dict.items():
all_si = True
for filter_name in self.filters:
if filter_name == "Videos":
# Lógica para videos
main_folder_path = os.path.join(self.launchbox_path, "Videos",
self.platform_combobox.get()).replace("\\", "/")
alternative_folder_path = os.path.join(self.alternative_path, self.platform_combobox.get(),
"Videos").replace("\\",
"/") if self.alternative_path else None
file_found = False
if alternative_folder_path and os.path.exists(alternative_folder_path):
file_path = os.path.join(alternative_folder_path, f"{search_names[0]}-01.mp4").replace("\\",
"/")
if os.path.isfile(file_path):
file_found = True
else:
if len(search_names) > 1:
file_path = os.path.join(alternative_folder_path, f"{search_names[1]}.mp4").replace(
"\\", "/")
if os.path.isfile(file_path):
file_found = True
if not file_found and os.path.exists(main_folder_path):
file_path = os.path.join(main_folder_path, f"{search_names[0]}-01.mp4").replace("\\", "/")
if os.path.isfile(file_path):
file_found = True
else:
if len(search_names) > 1:
file_path = os.path.join(main_folder_path, f"{search_names[1]}.mp4").replace("\\", "/")
if os.path.isfile(file_path):
file_found = True
if not file_found:
all_si = False
break
elif filter_name == "Manual":
# Lógica para manuales
main_folder_path = os.path.join(self.launchbox_path, "Manuals",
self.platform_combobox.get()).replace("\\", "/")
alternative_folder_path = os.path.join(self.alternative_path, self.platform_combobox.get(),
"Manuals").replace("\\",
"/") if self.alternative_path else None
file_found = False
if alternative_folder_path and os.path.exists(alternative_folder_path):
file_path = os.path.join(alternative_folder_path, f"{search_names[0]}-01.pdf").replace("\\",
"/")
if os.path.isfile(file_path):
file_found = True
else:
if len(search_names) > 1:
file_path = os.path.join(alternative_folder_path, f"{search_names[1]}.pdf").replace(
"\\", "/")
if os.path.isfile(file_path):
file_found = True
if not file_found and os.path.exists(main_folder_path):
file_path = os.path.join(main_folder_path, f"{search_names[0]}-01.pdf").replace("\\", "/")
if os.path.isfile(file_path):
file_found = True
else:
if len(search_names) > 1:
file_path = os.path.join(main_folder_path, f"{search_names[1]}.pdf").replace("\\", "/")
if os.path.isfile(file_path):
file_found = True
if not file_found:
all_si = False
break
else:
# Lógica para imágenes
image_folder = os.path.join(self.launchbox_path, "Images", self.platform_combobox.get(),
filter_name).replace("\\", "/")
image_found = False
if os.path.exists(image_folder):
for ext in ["png", "jpg"]:
image_path = os.path.join(image_folder, f"{search_names[0]}-01.{ext}").replace("\\", "/")
if os.path.isfile(image_path):
image_found = True
break
if len(search_names) > 1:
image_path = os.path.join(image_folder, f"{search_names[1]}.{ext}").replace("\\", "/")
if os.path.isfile(image_path):
image_found = True
break
if not image_found:
all_si = False
break
if not all_si:
filtered_game_dict[title] = search_names
return filtered_game_dict
def get_filter_value(self, search_names, filter_name):
image_folder = os.path.join(self.launchbox_path, "Images", self.platform_combobox.get(), filter_name).replace(
"\\", "/")
if os.path.exists(image_folder):
for search_name in search_names:
for ext in ["png", "jpg"]:
image_path = os.path.join(image_folder, f"{search_name}.{ext}").replace("\\", "/")
if os.path.isfile(image_path):
return search_name
return ""
if __name__ == "__main__":
try:
root = tk.Tk()
app = LaunchBoxManager(root)
root.mainloop()
except MemoryError:
print("Error de memoria grave. El programa debe cerrarse.")
except Exception as e:
print(f"Error inesperado: {e}")
messagebox.showerror("Error", f"Se produjo un error inesperado: {e}")