From 2d599141f9c51d959b07c8cc035285c0d0fcbf96 Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Mon, 7 Sep 2026 12:23:55 -0400 Subject: [PATCH] Add Logiqx XML DAT parsing (Redump) and clear stale file matches on re-ingest --- app/dat_parser.py | 88 ++++++++++++++++++++++++++++++++++++++--------- app/ingest.py | 6 ++++ 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/app/dat_parser.py b/app/dat_parser.py index 773bb40..65783ed 100644 --- a/app/dat_parser.py +++ b/app/dat_parser.py @@ -1,23 +1,29 @@ -"""Parser for clrmamepro (.dat) files. +"""Parser for clrmamepro / Logiqx XML (.dat) files. -Handles the clrmamepro text DAT format used by Redump and No-Intro:: +Supports both DAT layouts the project uses: - clrmamepro ( - name "Sony - PlayStation Portable" - version "2021.10.24" - ) - game ( - name "WipEout Pure (Europe)" - region "Europe" - serial "ULES-00011" - rom ( name "WipEout Pure (Europe).iso" size 123 crc ABCD1234 md5 ... sha1 ... ) - ) +* clrmamepro text DAT (No-Intro): parenthesised blocks, e.g.:: -``parse_dat_text`` returns ``(header, entries)``. Each entry is a dict with -scalar keys (``name``, ``description``, ``region``, ``serial``, ...) and -repeated nested blocks exposed as lists under their block key (``rom``). + clrmamepro ( + name "Sony - PlayStation Portable" + version "2021.10.24" + ) + game ( + name "WipEout Pure (Europe)" + region "Europe" + serial "ULES-00011" + rom ( name "WipEout Pure (Europe).iso" size 123 crc ABCD1234 md5 ... sha1 ... ) + ) + +* Logiqx XML DAT (Redump): ```` elements with ```` children. + +Both ``parse_dat_text`` and ``parse_dat_xml`` return ``(header, entries)``. +Each entry is a dict with scalar keys (``name``, ``description``, ``region``, +``serial``, ...) and repeated nested blocks exposed as lists under their block +key (``rom``). """ import re +import xml.etree.ElementTree as ET from pathlib import Path _TOKEN_RE = re.compile( @@ -124,5 +130,55 @@ def parse_dat_text(text): return header, entries +def _xml_text(element, key): + child = element.find(key) + if child is not None and child.text: + return child.text.strip() + return None + + +def parse_dat_xml(text): + """Parse a Logiqx XML DAT (the format redump.org uses). + + Returns ``(header, entries)`` in the same shape as :func:`parse_dat_text`. + """ + root = ET.fromstring(text) + + header = {} + header_el = root.find("header") + if header_el is not None: + for child in header_el: + if child.text: + header[child.tag] = child.text.strip() + + entries = [] + for game in root.iter(): + if game.tag not in ("game", "machine"): + continue + entry = {"name": (game.get("name") or "").strip()} + for key in ("description", "serial", "region", "category"): + value = _xml_text(game, key) + if value: + entry[key] = value + entry["rom"] = [ + { + "name": rom.get("name", ""), + "size": rom.get("size"), + "crc": rom.get("crc"), + "md5": rom.get("md5"), + "sha1": rom.get("sha1"), + "serial": rom.get("serial"), + } + for rom in game.findall("rom") + ] + entries.append(entry) + + return header, entries + + def parse_dat_file(path): - return parse_dat_text(Path(path).read_text(encoding="utf-8-sig")) + text = Path(path).read_text(encoding="utf-8-sig") + stripped = text.lstrip() + if stripped.startswith("