Created
December 11, 2021 14:15
-
-
Save CookiePLMonster/6ee69667586c74db66a35d85a4c5a5df to your computer and use it in GitHub Desktop.
Migrate LaunchBox playtime from the PlaytimeTracker plugin to a native XML entry
This file contains 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
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