Created
March 17, 2018 09:49
-
-
Save cmitu/2f1a0dab4e5086b8fca99b081677b6ec to your computer and use it in GitHub Desktop.
Exports one gamelist xml file to Excel.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
''' | |
Script to export one gamelist.xml file to an Excel spreadsheet. | |
Without arguments | |
* it searches for a `gamelist.xml` file in the running dir | |
* outputs a `gamelist.xlsx` file in the running dir | |
''' | |
import xml.etree.ElementTree as et | |
import logging as log | |
import os.path | |
import fnmatch | |
import argparse | |
import xlsxwriter | |
from datetime import datetime as dt | |
# Set up logging using the logging module. | |
log.basicConfig(level=log.INFO, format=u"%(asctime)s %(levelname)-6s %(message)s") | |
logger = log.getLogger(__name__) | |
# Date time format | |
DATE_TIME_FORMAT = "%Y%m%dT%H%M%S" | |
def get_xml_element_text(xml, node_name): | |
if xml.find(node_name) is None or xml.find(node_name).text is None: | |
return None | |
else: | |
return xml.find(node_name).text.strip() | |
def is_number(s): | |
try: | |
int(s) | |
return True | |
except: | |
return False | |
def is_float(s): | |
try: | |
float(s) | |
return True | |
except: | |
return False | |
def get_xml_element_bool(xml, node_name): | |
""" | |
Returns either yes or None, depending on the value of the @parm node_name. | |
""" | |
if xml.find(node_name) is None: | |
return None | |
elif xml.find(node_name).text.lower() == "false" or xml.find(node_name).text.lower() == "no": | |
return None | |
else: | |
return "yes" | |
def get_xml_element_date(xml, node_name): | |
""" | |
Returns a DateTime or a String, depending on the value of the @parm node_name. | |
""" | |
global DATE_TIME_FORMAT | |
if not xml.find(node_name) is None and not xml.find(node_name).text is None: | |
date_text = xml.find(node_name).text | |
# Release date can appear as both ISO date or just as an year. | |
# If it's an ISO date, then try to convert it, otherwise just return the text | |
if len(date_text) < 6: | |
return date_text | |
else: | |
try: | |
date = dt.strptime(xml.find(node_name).text, DATE_TIME_FORMAT) | |
return date | |
except ValueError: | |
return date_text | |
else: | |
return None | |
def get_xml_element_int(xml, node_name): | |
""" | |
Returns None or a Number, depending on the value of the @parm. | |
""" | |
if xml.find(node_name) is None: | |
return None | |
else: | |
try: | |
return int(xml.find(node_name).text) | |
except ValueError: | |
return xml.find(node_name).text | |
except TypeError: | |
return None | |
class System(object): | |
""" | |
Class that models an ES System, storing the attributes of the System and its list of Games | |
""" | |
info_keys = ("name") | |
def __init__(self, name): | |
self.info = { 'name': name } | |
self.games = [] # List of games | |
def __str__(self): | |
return self.info['name'] + ", games: " + str(len(self.games)) | |
class Game: | |
info_keys = ['name', 'gametype', 'genre', 'version', 'originaltitle', 'alternatetitle', 'desc', 'publisher', 'developer', 'hackedby', 'translatedby'] | |
info_keys += ['path', 'playerstext', 'releasetext'] | |
info_keys += ['video', 'marquee', 'thumbnail'] | |
info_keys += ['region', 'platform', 'media', 'controller'] | |
info_keys += ['boxfront', 'cart', 'title', 'action', 'threedbox'] | |
info_keys += ['gamefaq', 'manual', 'vgmap', 'license', 'programmer', 'musician'] | |
info_date = ['releasedate', 'hackreleasedate', 'transreleasedate'] | |
info_int = ['players'] | |
@staticmethod | |
def get_headers(): | |
return (Game.info_keys + Game.info_date + Game.info_int) | |
def __init__(): | |
self.info = dict.fromkeys(Game.get_headers()) | |
def __init__(self, obj): | |
self.info = dict.fromkeys(Game.info_keys) | |
# Get the text metadata | |
for attr in self.info.keys(): | |
self.info[attr] = get_xml_element_text(obj, attr) | |
# Get the date metadata | |
for attr in Game.info_date: | |
self.info[attr] = get_xml_element_date(obj, attr) | |
# Get the integer metadata | |
for attr in Game.info_int: | |
self.info[attr] = get_xml_element_int(obj, attr) | |
def __str__(self): | |
return str("{0}\t{1}".format(self.info["name"]), str(self.info["path"])) | |
def check_rom(rom_folder, rom_path): | |
""" | |
Method to check if a ROM is present in the filesystem. | |
Returns true if the ROM is present, false otherwise. | |
""" | |
# The Rom path in the gamelist might be absolute or relative. | |
# Check if the path begins with an '/' to decide if it's an absolute path. | |
path_to_check = rom_path | |
if not rom_path.startswith('/'): | |
path_to_check = rom_folder + "/" + rom_path | |
return os.path.isfile(path_to_check) | |
def get_rom_path(rom_folder, rom_path): | |
if not rom_path.startswith('/'): | |
path_to_check = rom_folder + "/" + rom_path | |
return os.path.realpath(path_to_check) | |
def parse_gamelist(gamelist_path = "gamelist.xml"): | |
s = System("games") | |
systems = [] | |
try: | |
gamelist = et.parse(gamelist_path) | |
except IOError: | |
logger.warn("Could not open the gamelist file %s !", gamelist_path) | |
exit(1) | |
except et.ParseError as v: | |
logger.error("Incorrect XML file: %s", format(v)) | |
exit(1) | |
# Ok, we have the gamelist, get each game and parse it. | |
for game in gamelist.findall('game'): | |
rom = Game(game) | |
logger.debug('Found - %s', rom.info['name']) | |
s.games.append(rom) | |
# If we have more than 1 ROM in the system, add it to the exported list | |
if len(s.games) > 0: | |
systems.append(s) | |
else: | |
logger.debug( | |
"System %s has no games/roms, it's excluded from the export", s.info['name']) | |
return systems | |
# Export the system list to excel | |
def xlsx_export_workbook(systems, output='export.xlsx'): | |
if not len(systems): | |
raise "Exported system list is empty" | |
return | |
# Create the Workbook | |
wb = xlsxwriter.Workbook(output, | |
{'default_date_format': 'dd-mm-yyyy', | |
'in_memory': True, | |
}) | |
# Add some metadata to it | |
wb.set_properties({ | |
'title': 'Game List Export', | |
'subject': 'Game List Export', | |
'category': 'Gaming', | |
'author': "XlsxWriter (github.com/jmcnamara/XlsxWriter), version " + xlsxwriter.__version__, | |
}) | |
wb.set_custom_property('Date Exported', dt.now()) | |
fmt_bold = wb.add_format({'bold': True}) | |
fmt_bold_2 = wb.add_format({'bold': True, 'bg_color': 'red', 'color': 'white'}) | |
fmt_sys_header = wb.add_format({'bold': True, 'bg_color': 'green', 'color': 'white'}) | |
fmt_fav_row = wb.add_format({'bg_color': '#FFCC7C'}) | |
table_headers = list(map(lambda x: {'header': str(x).capitalize()}, Game.get_headers())) | |
for i, s in enumerate(systems): | |
# Add a worksheet for each system. | |
b = wb.add_worksheet(s.info['name']) | |
# Create a table with each system and the # of games detected in each system. | |
# Print the table header | |
b.set_column(0, 0, 50) | |
t = b.add_table(0, 0, len(s.games), len(Game.get_headers()) - 1, | |
{ | |
'style': 'Table Style Medium 7', | |
'columns': table_headers, | |
# The name of the Table should only containt letters + numbers. | |
# 'name'c: s.info["name"].replace('[^[a-zA-Z0-9]', ''), | |
'autofilter': False, | |
'banded_rows': False, | |
}) | |
# Print the table rows | |
for j, g in enumerate(s.games): | |
xlsx_export_system_row(wb, b, j+1, g) | |
# Close the workbook | |
wb.close() | |
def xlsx_export_system_row(workbook, sheet, row_number, game, system_name=None): | |
fmt_fav = workbook.add_format({'align': 'center'}) | |
# On special collections, 1st column is the name of the system where the game belongs | |
# Only shown when set. | |
if system_name is not None: | |
sheet.write(row_number, 0, system_name) | |
offset = 1 | |
else: | |
offset = 0 | |
for column, header in enumerate(Game.get_headers()): | |
if header in Game.info_date and type(game.info[header]).__name__ == "datetime": | |
sheet.write_datetime(row_number, column + offset, game.info[header]) | |
elif header in ('playcount', 'players') and is_number(game.info[header]): | |
sheet.write_number(row_number, column + offset, int(game.info[header])) | |
elif header in ('rating',) and is_float(game.info[header]): | |
sheet.write_number(row_number, column + offset, float(game.info[header])) | |
elif header.lower() in ('favorite', 'kidgame', 'hidden'): | |
sheet.write(row_number, column + offset, game.info[header], fmt_fav) | |
else: | |
sheet.write(row_number, column + offset, game.info[header]) | |
# If we're on the 'All' sheet, add the description of the game in the cell comments | |
if sheet.get_name().lower() == "all" and header.lower() == "name" and not game.info['desc'] is None: | |
sheet.write_comment(row_number, column + offset, | |
game.info['desc'], {'x_scale': 4, 'y_scale': 4}) | |
def parse_arguments(): | |
parser = argparse.ArgumentParser( | |
description='Export an XML formatted file to an Excel spreadsheet') | |
parser.add_argument('input', nargs='?', | |
default="gamelist.xml", | |
help="Gamelist file to parse (default is 'gamelist.xml'") | |
parser.add_argument('output', nargs='?', | |
default="gamelist.xlsx", | |
help="Export file (default is 'gamelist.xlsx')") | |
parser.add_argument('-d', '--debug', action='store_true', | |
help="run script with with debug info", default=False) | |
args = parser.parse_args() | |
return (args.input, args.output, args.debug) | |
if __name__ == "__main__": | |
# Parse arguments | |
(input, output, debug) = parse_arguments() | |
# Set logging level; default is INFO, add debugging if requested via parameter | |
if debug: | |
logger.setLevel(log.DEBUG) | |
logger.debug("Starting") | |
systems = parse_gamelist(input) | |
# See how many games we have | |
total_games = sum(map(lambda system: len(system.games), systems)) | |
logger.info("Total games after parsing gamelist files - " + str(total_games)) | |
if total_games < 1: | |
logger.warn("No games to export, exiting..") | |
exit(1) | |
logger.info("Exporting to file %s",output) | |
xlsx_export_workbook(systems, output) | |
logger.debug("Finished") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment