Last active
December 10, 2022 21:31
-
-
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
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
#!/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