Add Logiqx XML DAT parsing (Redump) and clear stale file matches on re-ingest
This commit is contained in:
+62
-6
@@ -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 (
|
||||
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 ... )
|
||||
)
|
||||
|
||||
``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``).
|
||||
* Logiqx XML DAT (Redump): ``<game>`` elements with ``<rom>`` 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("<?xml") or stripped.startswith("<"):
|
||||
return parse_dat_xml(text)
|
||||
return parse_dat_text(text)
|
||||
|
||||
@@ -37,6 +37,12 @@ def ingest_dat(conn, path):
|
||||
existing = conn.execute("SELECT id FROM dats WHERE name = ?", (dat_name,)).fetchone()
|
||||
if existing:
|
||||
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 releases WHERE dat_id = ?", (dat_id,))
|
||||
conn.execute(
|
||||
|
||||
Reference in New Issue
Block a user