Skip to content

Instantly share code, notes, and snippets.

@CookiePLMonster
Created December 11, 2021 14:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CookiePLMonster/6ee69667586c74db66a35d85a4c5a5df to your computer and use it in GitHub Desktop.
Save CookiePLMonster/6ee69667586c74db66a35d85a4c5a5df to your computer and use it in GitHub Desktop.
Migrate LaunchBox playtime from the PlaytimeTracker plugin to a native XML entry
import os
import re
import sys
import datetime
import xml.etree.ElementTree as ET
PLATFORMS_DIR = 'Data/Platforms'
PLAYTIME_PLUGIN_DIR = 'Plugins/PlaytimeTracker'
if len(sys.argv) > 1:
# Prepend the path to LaunchBox if it was passed
PLATFORMS_DIR = os.path.join(sys.argv[1], PLATFORMS_DIR)
PLAYTIME_PLUGIN_DIR = os.path.join(sys.argv[1], PLAYTIME_PLUGIN_DIR)
GAME_NODE = 'Game'
ID_NODE = 'ID'
GAMEID_NODE = 'GameID'
TITLE_NODE = 'Title'
PLAYTIME_NODE = 'PlayTime'
def indent(elem, level=0):
i = '\n' + level*" "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
indent(elem, level+1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
with os.scandir(PLATFORMS_DIR) as it:
for entry in it:
if os.path.splitext(entry.name)[1].lower() == '.xml':
print(f'Reading {entry.name}...')
tree = ET.parse(entry.path)
root = tree.getroot()
# For each <Game> entry, we try to find a matching .txt file in Plugin\PlaytimeTracker
# If it exists, read the time and place it in a <PlayTime> node, then delete any <CustomField>
# with children node <Name> equaling "Playtime"
custom_fields_to_delete = []
for game in root.findall(GAME_NODE):
id_node = game.find(ID_NODE)
if id_node is not None:
id = id_node.text
try:
with open(os.path.join(PLAYTIME_PLUGIN_DIR, id + '.txt'), 'r') as f:
match = re.match(r"(\d+):(\d+):(\d+):(\d+)", f.read())
if match is not None:
delta = datetime.timedelta(
days=int(match[1]),
hours=int(match[2]),
minutes=int(match[3]),
seconds=int(match[4]))
delta_seconds = int(delta.total_seconds())
# If there is a Title field, pretty print that for the user's information
title_node = game.find(TITLE_NODE)
if title_node is not None:
print(f'\t{title_node.text}: {delta} ({delta_seconds} second(s))')
# Add or replace the entry
playtime_node = game.find(PLAYTIME_NODE)
if playtime_node is None:
playtime_node = ET.Element(PLAYTIME_NODE)
game.append(playtime_node)
playtime_node.text = str(delta_seconds)
custom_fields_to_delete.append(id)
except FileNotFoundError:
pass
# Clean up CustomField Playtime entries for games we obtained playtime of
# (hopefully that means all Playtime entries)
print('Cleaning up obsolete Playtime custom fields...', end=' ')
num_cleaned = 0
for custom_playtime in root.findall("./CustomField[Name='Playtime']"):
gameid = custom_playtime.find(GAMEID_NODE)
if gameid is not None and gameid.text in custom_fields_to_delete:
root.remove(custom_playtime)
num_cleaned += 1
print(f'{num_cleaned} entries removed')
# Write the XML in the same format as LaunchBox does
indent(root)
tree.write(entry.path, encoding='utf-8', xml_declaration=False)
# Prepend the XML declaration, this is the best way to let XML generator
# deal with newlines, while retaining the declaration
with open(entry.path, 'r+', encoding='utf-8') as f:
content = f.read()
f.seek(0)
f.write('<?xml version="1.0" standalone="yes"?>\n')
f.write(content.rstrip())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment