Add Logiqx XML DAT parsing (Redump) and clear stale file matches on re-ingest

This commit is contained in:
2026-09-07 16:20:39 -04:00
parent 922c01d17c
commit 2d599141f9
2 changed files with 78 additions and 16 deletions
+62 -6
View File
@@ -1,6 +1,8 @@
"""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 text DAT (No-Intro): parenthesised blocks, e.g.::
clrmamepro ( clrmamepro (
name "Sony - PlayStation Portable" name "Sony - PlayStation Portable"
@@ -13,11 +15,15 @@ Handles the clrmamepro text DAT format used by Redump and No-Intro::
rom ( name "WipEout Pure (Europe).iso" size 123 crc ABCD1234 md5 ... sha1 ... ) rom ( name "WipEout Pure (Europe).iso" size 123 crc ABCD1234 md5 ... sha1 ... )
) )
``parse_dat_text`` returns ``(header, entries)``. Each entry is a dict with * Logiqx XML DAT (Redump): ``<game>`` elements with ``<rom>`` children.
scalar keys (``name``, ``description``, ``region``, ``serial``, ...) and
repeated nested blocks exposed as lists under their block key (``rom``). 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 re
import xml.etree.ElementTree as ET
from pathlib import Path from pathlib import Path
_TOKEN_RE = re.compile( _TOKEN_RE = re.compile(
@@ -124,5 +130,55 @@ def parse_dat_text(text):
return header, entries 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): 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("<?xml") or stripped.startswith("<"):
return parse_dat_xml(text)
return parse_dat_text(text)
+6
View File
@@ -37,6 +37,12 @@ def ingest_dat(conn, path):
existing = conn.execute("SELECT id FROM dats WHERE name = ?", (dat_name,)).fetchone() existing = conn.execute("SELECT id FROM dats WHERE name = ?", (dat_name,)).fetchone()
if existing: if existing:
dat_id = existing["id"] dat_id = existing["id"]
# Files that resolved to a release in this DAT must be re-identified
# against the new catalog (the release ids are about to be recycled).
conn.execute(
"UPDATE files SET release_id = NULL WHERE release_id IN (SELECT id FROM releases WHERE dat_id = ?)",
(dat_id,),
)
conn.execute("DELETE FROM roms WHERE release_id IN (SELECT id FROM releases WHERE dat_id = ?)", (dat_id,)) conn.execute("DELETE FROM roms WHERE release_id IN (SELECT id FROM releases WHERE dat_id = ?)", (dat_id,))
conn.execute("DELETE FROM releases WHERE dat_id = ?", (dat_id,)) conn.execute("DELETE FROM releases WHERE dat_id = ?", (dat_id,))
conn.execute( conn.execute(