Skip to content

Instantly share code, notes, and snippets.

@ssokolow
Last active December 10, 2022 21:31
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 ssokolow/e1c4b6fceb65bcf4e6d886ce691280bf to your computer and use it in GitHub Desktop.
Save ssokolow/e1c4b6fceb65bcf4e6d886ce691280bf to your computer and use it in GitHub Desktop.
Script to reset play statistics on EmulationStation-based distros like RetroPie and Batocera
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""A helper to reset play statistics in gamelist.xml files.
Originally developed for preparing a Batocera Linux device to be installed as a
gift to the family after it had been recording play statistics in response to
QA and integration testing, but should work on any EmulationStation-based
distro.
--snip--
Installation Instructions:
1. Copy the link from the Raw button at
https://gist.github.com/ssokolow/e1c4b6fceb65bcf4e6d886ce691280bf
1. cd /path/to/roms/folder
2. wget <PASTE THE LINK>
3. chmod +x reset_play_statistics.py
4. ./reset_play_statistics.py
Run with the --help argument for information on advanced usage.
"""
__author__ = "Stephan Sokolow (deitarion/SSokolow)"
__appname__ = "Reset Play Statistics"
__version__ = "0.1"
__license__ = "MIT"
import logging, os, sys
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from typing import List, Optional
from xml.etree import ElementTree as ET # nosec
DEFAULT_REMOVE_TAGS = ['playcount', 'lastplayed', 'gametime']
USAGE_MSG = r"""
I could not find your "roms" folder. Please do ONE of the following:
a. Put me next to your roms folder
/home/pi/RetroPie/{basename} (RetroPie)
/userdata/{basename} (Batocera)
...
b. Put me in your roms folder
/home/pi/RetroPie/roms/{basename} (RetroPie)
/userdata/roms/{basename} (Batocera)
...
c. Pass your roms folder or a specific gamelist.xml file path as an argument
{argv0} /path/to/roms
{argv0} /path/to/roms/platform/gamelist.xml
...
You may pass more than one path with option C.
""".format(argv0=sys.argv[0], basename=os.path.basename(sys.argv[0]))
log = logging.getLogger(__name__)
def find_roms() -> Optional[str]:
"""Find the 'roms' folder if it's a sibling or parent directory"""
parent = os.path.dirname(os.path.abspath(__file__))
sibling = os.path.join(parent, 'roms')
if os.path.isdir(sibling):
return sibling
if os.path.basename(parent) == 'roms':
return parent
return None
def process_file(xml_path: str, remove_tags: List[str], dry_run: bool):
"""Process a single gameinfo.xml file"""
changed = False
try:
tree = ET.parse(xml_path)
except ET.ParseError:
log.error("Could not parse file. Make sure it's a gamelist.xml: %s",
xml_path)
return
log.info("Processing %s", xml_path)
for node in tree.getroot().findall('.//game'):
name_node = node.find('name')
name = name_node.text if name_node is not None else '(no name)'
for tag in remove_tags:
for match in node.findall(tag):
log.debug("Removing tag %r for %r from file %s",
tag, name, xml_path)
if not dry_run:
changed = True
node.remove(match)
if changed and not dry_run:
tree.write(xml_path, xml_declaration=True)
def process_arg(path: str, remove_tags: List[str], dry_run: bool):
"""Helper to allow both file and directories to be given as arguments"""
log.info("Will remove tags: %s", ', '.join(remove_tags))
path = os.path.abspath(path) # For nicer log messages
if os.path.isfile(path):
process_file(path, remove_tags, dry_run)
elif os.path.isdir(path):
for parent, dirs, files in os.walk(path):
dirs.sort()
for fname in files:
if fname == 'gamelist.xml':
fpath = os.path.join(parent, fname)
process_file(fpath, remove_tags, dry_run)
def main():
"""The main entry point, compatible with setuptools entry points."""
parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter,
description=__doc__.replace('\r\n', '\n').split('\n--snip--\n')[0])
parser.add_argument('-V', '--version', action='version',
version="%%(prog)s v%s" % __version__)
parser.add_argument('-v', '--verbose', action="count",
default=3, help="Increase the verbosity. Repeat for extra effect.")
parser.add_argument('-q', '--quiet', action="count",
default=0, help="Decrease the verbosity. Repeat for extra effect.")
parser.add_argument('-n', '--dry-run', action="store_true",
default=False, help="Don't modify anything. Just print.")
parser.add_argument('--remove-tag', action='append', metavar="TAG",
help="Override the list of tags to be removed. May be specified "
"multiple times to build up a new list. "
"(default: {}".format(', '.join(DEFAULT_REMOVE_TAGS)))
parser.add_argument('path', action="store", nargs="*",
help="Path to operate on")
args = parser.parse_args()
# Set up clean logging to stderr
log_levels = [logging.CRITICAL, logging.ERROR, logging.WARNING,
logging.INFO, logging.DEBUG]
args.verbose = min(args.verbose - args.quiet, len(log_levels) - 1)
args.verbose = max(args.verbose, 0)
logging.basicConfig(level=log_levels[args.verbose],
format='%(levelname)s: %(message)s')
remove_tags = args.remove_tag or DEFAULT_REMOVE_TAGS
if args.path:
for path in args.path:
process_arg(path, remove_tags, args.dry_run)
else:
roms_dir = find_roms()
if roms_dir:
process_arg(roms_dir, remove_tags, args.dry_run)
else:
print(USAGE_MSG)
return
log.info("Note that EmulationStation will cache recent changes in memory "
"and you may need to use Start > Game Settings > Update Gamelists "
"before running this script in order to get the expected results.")
if __name__ == '__main__': # pragma: nocover
main()
# vim: set sw=4 sts=4 expandtab :
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment