# Script para copiar roms a partir de un xml de fbneo # Copia las roms por desarrollador y sin clones # Por hacer: # pasar por parametro si se quieren clones import os import shutil import sys import argparse import config from xml.dom import minidom def normalize_path(path): """Elimina los caracteres ilegales de la cadena de texto""" # Lista de caracteres ilegales que deben ser eliminados illegal_chars = ["<", ">", ":", '"', "/", "\\", "|", "?", "*"] # Carácter de reemplazo replace_with = "_" # Reemplaza cada carácter ilegal en la cadena de texto for char in illegal_chars: path = path.replace(char, replace_with) return path def parse_arguments(): """Parsea los argumentos de la línea de comandos""" parser = argparse.ArgumentParser( description="Gestiona las roms de fbneo por desarrolladores a partir de un fichero .dat" ) # Grupo de argumentos mutuos exclusivos group = parser.add_mutually_exclusive_group() group.add_argument( "-l", "--list", help="Muestra la lista de desarrolladores o de juegos", action="store_true", ) group.add_argument( "-c", "--copy", help="Copia las roms", action="store_true" ) group.add_argument( "-f", "--find", help="Busca juegos que contengan la cadena de texto especificada", ) # Otros argumentos parser.add_argument( "-s", "--sort", help="Ordena los resultados por carpetas de desarrollador al copiar todas las roms", action="store_true", ) parser.add_argument( "-cf", "--create_folder", help="Copia las roms dentro de la carpeta del desarrollador", action="store_true", ) parser.add_argument( "-m", "--manufacturer", help="Selecciona un desarrollador" ) parser.add_argument( "-d", "--dat", help="Ruta del fichero .dat con información de las roms" ) parser.add_argument( "-i", "--input", help="Ruta donde se encuentran las roms", default="" ) parser.add_argument( "-o", "--output", help="Ruta donde depositar las roms", default="" ) return parser.parse_args() def load_dat_file(dat_path): """Carga y analiza el fichero .dat especificado""" if not dat_path: print("No se ha especificado un fichero .dat") if os.path.exists(config.XML_FILE): # Usa el fichero por defecto si no se especifica uno print(f"Usando el fichero por defecto: {config.XML_FILE}") dat_path = config.XML_FILE else: # Salir si no se encuentra el fichero por defecto sys.exit(2) elif not os.path.isfile(dat_path): # Salir si el fichero especificado no existe print(f"No se encuentra el fichero .dat especificado: {dat_path}") sys.exit(2) print(f"Analizando el fichero {dat_path}") return minidom.parse(dat_path) def list_manufacturers(file): """Lista todos los desarrolladores presentes en el fichero .dat""" print("Lista de todos los desarrolladores:") manufacturers = [] games = file.getElementsByTagName("game") # Recoge todos los desarrolladores de los juegos for game in games: manufacturer = game.getElementsByTagName("manufacturer")[0] manufacturers.append(manufacturer.firstChild.data) # Elimina los duplicados manufacturers = list(dict.fromkeys(manufacturers)) # Ordena alfabéticamente la lista de desarrolladores manufacturers.sort() # Imprime cada desarrollador en una nueva línea for manufacturer in manufacturers: print(manufacturer) def list_games_by_manufacturer(file, manufacturer): """Lista todos los juegos de un desarrollador específico""" games = file.getElementsByTagName("game") filtered_games = [] # Filtra los juegos según el fabricante especificado y excluye clones y BIOS for game in games: cloneof = game.getAttribute("cloneof") isbios = game.getAttribute("isbios") game_manufacturer = game.getElementsByTagName("manufacturer")[0] description = game.getElementsByTagName("description")[0] if ( (game_manufacturer.firstChild.data == manufacturer or manufacturer == 'all') and not cloneof and not isbios ): filtered_games.append((description.firstChild.data, game.getAttribute('name'), game_manufacturer.firstChild.data)) # Encuentra el ancho máximo para cada columna max_desc_length = max(len("Descripción"), max(len(row[0]) for row in filtered_games)) max_name_length = max(len("Nombre del archivo"), max(len(row[1] + '.zip') for row in filtered_games)) max_manufacturer_length = max(len("Desarrollador"), max(len(row[2]) for row in filtered_games)) # Imprime el encabezado de la tabla print(f"Lista de todos los juegos de {manufacturer}: {len(filtered_games)}") print(f"{'='*(max_desc_length+max_name_length+max_manufacturer_length+10)}") print(f"| {'Descripción':<{max_desc_length}} | {'Nombre del archivo':<{max_name_length}} | {'Desarrollador':<{max_manufacturer_length}} |") print(f"|{'-'*(max_desc_length+2)}|{'-'*(max_name_length+2)}|{'-'*(max_manufacturer_length+2)}|") # Imprime cada juego en la tabla for description, name, game_manufacturer in filtered_games: print(f"| {description:<{max_desc_length}} | {name + '.zip':<{max_name_length}} | {game_manufacturer:<{max_manufacturer_length}} |") # Imprime el pie de la tabla print(f"{'='*(max_desc_length+max_name_length+max_manufacturer_length+10)}") def find_games(file, search_string): """Busca juegos que contengan la cadena de texto especificada""" games = file.getElementsByTagName("game") filtered_games = [] # Filtra los juegos según la cadena de búsqueda especificada for game in games: description = game.getElementsByTagName("description")[0] if search_string.casefold() in description.firstChild.data.casefold(): game_manufacturer = game.getElementsByTagName("manufacturer")[0] filtered_games.append((description.firstChild.data, game.getAttribute('name'), game_manufacturer.firstChild.data)) if not filtered_games: print(f"No se encontraron juegos que contengan '{search_string}'") return # Encuentra el ancho máximo para cada columna max_desc_length = max(len("Descripción"), max(len(row[0]) for row in filtered_games)) max_name_length = max(len("Nombre del archivo"), max(len(row[1] + '.zip') for row in filtered_games)) max_manufacturer_length = max(len("Desarrollador"), max(len(row[2]) for row in filtered_games)) # Imprime el encabezado de la tabla print(f"Juegos que contienen '{search_string}': {len(filtered_games)}") print(f"{'='*(max_desc_length+max_name_length+max_manufacturer_length+10)}") print(f"| {'Descripción':<{max_desc_length}} | {'Nombre del archivo':<{max_name_length}} | {'Desarrollador':<{max_manufacturer_length}} |") print(f"|{'-'*(max_desc_length+2)}|{'-'*(max_name_length+2)}|{'-'*(max_manufacturer_length+2)}|") # Imprime cada juego en la tabla for description, name, game_manufacturer in filtered_games: print(f"| {description:<{max_desc_length}} | {name + '.zip':<{max_name_length}} | {game_manufacturer:<{max_manufacturer_length}} |") # Imprime el pie de la tabla print(f"{'='*(max_desc_length+max_name_length+max_manufacturer_length+10)}") def copy_games(file, args, all_games=False): """Copia los juegos según los criterios especificados""" if args.create_folder and not all_games: # Normaliza el nombre del desarrollador para usarlo como nombre de carpeta normalized_manufacturer = normalize_path(args.manufacturer) os.mkdir(os.path.join(args.output, args.manufacturer)) # Listas para almacenar los juegos no encontrados, ignorados y copiados notfound = [] ignored_games = [] copied_games = [] games = file.getElementsByTagName("game") if all_games: print(f"Copiando todos los juegos: {len(games)}") else: print(f"Copiando todos los juegos de {args.manufacturer}: {len(games)}") for game in games: isignored = False name = game.getAttribute("name") + ".zip" cloneof = game.getAttribute("cloneof") isbios = game.getAttribute("isbios") romof = game.getAttribute("romof") manufacturer = game.getElementsByTagName("manufacturer")[0] description = game.getElementsByTagName("description")[0] driver = game.getElementsByTagName("driver") status = ( game.getElementsByTagName("driver")[0].getAttribute("status") if driver else "" ) # Verifica si el juego cumple con los criterios para ser copiado if (all_games or manufacturer.firstChild.data == args.manufacturer) and not cloneof and not isbios and status == "good": # Verifica si el juego debe ser ignorado for ignore_element in config.IGNORE_LIST: if ignore_element.casefold() in description.firstChild.data.casefold(): ignored_games.append(description.firstChild.data) isignored = True for ignore_element in config.IGNORE_SYSTEM: if ignore_element.casefold() in romof.casefold(): ignored_games.append(description.firstChild.data) isignored = True if not isignored: # Define la ruta de origen y destino del archivo src = os.path.join(args.input, name) if args.sort: x = manufacturer.firstChild.data x = x.replace(r"/", r"-") dst = os.path.join(args.output, x, name) if not os.path.isdir(os.path.join(args.output, x)): os.mkdir(os.path.join(args.output, x)) else: if args.create_folder and not all_games: dst = os.path.join(args.output, normalized_manufacturer, name) else: dst = os.path.join(args.output, name) # Copia el archivo si existe en la ruta de origen if os.path.isfile(src): shutil.copyfile(src, dst) copied_games.append(description.firstChild.data) print(description.firstChild.data) else: notfound.append(description.firstChild.data) # Imprime los resultados de los juegos no encontrados, ignorados y copiados print(f"\nJuegos faltantes: {len(notfound)}") for game in notfound: print(game) print(f"\nJuegos ignorados: {len(ignored_games)}") for game in ignored_games: print(game) print(f"\nJuegos copiados: {len(copied_games)}") print(f"Juegos faltantes: {len(notfound)}") print(f"Juegos ignorados: {len(ignored_games)}") def main(): """Función principal que coordina la ejecución del script""" # Parsear los argumentos de la línea de comandos args = parse_arguments() # Cargar y analizar el fichero .dat file = load_dat_file(dat_path=args.dat) # Manejar la opción de listar desarrolladores o juegos if args.list: if args.manufacturer: list_games_by_manufacturer(file=file, manufacturer=args.manufacturer) else: list_manufacturers(file=file) # Manejar la opción de copiar roms if args.copy: if args.manufacturer: copy_games(file=file, args=args, all_games=False) else: copy_games(file=file, args=args, all_games=True) # Manejar la opción de búsqueda de juegos if args.find: find_games(file=file, search_string=args.find) if __name__ == "__main__": main()