feat: add batocera, recalbox, retrobat native exporters

This commit is contained in:
Abdessamad Derraz
2026-03-30 15:31:44 +02:00
parent 9b785ec785
commit 1e6b499602
4 changed files with 361 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
"""Exporter for Batocera batocera-systems format (Python dict)."""
from __future__ import annotations
from pathlib import Path
from .base_exporter import BaseExporter
class Exporter(BaseExporter):
"""Export truth data to Batocera batocera-systems format."""
@staticmethod
def platform_name() -> str:
return "batocera"
def export(
self,
truth_data: dict,
output_path: str,
scraped_data: dict | None = None,
) -> None:
native_map: dict[str, str] = {}
if scraped_data:
for sys_id, sys_data in scraped_data.get("systems", {}).items():
nid = sys_data.get("native_id")
if nid:
native_map[sys_id] = nid
lines: list[str] = [
"#!/usr/bin/env python3",
"# Generated batocera-systems BIOS declarations",
"from collections import OrderedDict",
"",
"systems = {",
]
systems = truth_data.get("systems", {})
for sys_id in sorted(systems):
sys_data = systems[sys_id]
files = sys_data.get("files", [])
if not files:
continue
native_id = native_map.get(sys_id, sys_id)
lines.append(f' "{native_id}": {{')
lines.append(' "biosFiles": [')
for fe in files:
name = fe.get("name", "")
if name.startswith("_"):
continue
dest = fe.get("destination", name)
md5 = fe.get("md5", "")
if isinstance(md5, list):
md5 = md5[0] if md5 else ""
lines.append(" {")
lines.append(f' "file": "bios/{dest}",')
lines.append(f' "md5": "{md5}",')
lines.append(" },")
lines.append(" ],")
lines.append(" },")
lines.append("}")
lines.append("")
Path(output_path).write_text("\n".join(lines), encoding="utf-8")
def validate(self, truth_data: dict, output_path: str) -> list[str]:
content = Path(output_path).read_text(encoding="utf-8")
issues: list[str] = []
for sys_data in truth_data.get("systems", {}).values():
for fe in sys_data.get("files", []):
name = fe.get("name", "")
if name.startswith("_"):
continue
if name not in content:
issues.append(f"missing: {name}")
return issues

View File

@@ -0,0 +1,90 @@
"""Exporter for Recalbox es_bios.xml format."""
from __future__ import annotations
from pathlib import Path
from xml.etree.ElementTree import Element, SubElement, ElementTree, indent
from .base_exporter import BaseExporter
class Exporter(BaseExporter):
"""Export truth data to Recalbox es_bios.xml format."""
@staticmethod
def platform_name() -> str:
return "recalbox"
def export(
self,
truth_data: dict,
output_path: str,
scraped_data: dict | None = None,
) -> None:
native_map: dict[str, str] = {}
if scraped_data:
for sys_id, sys_data in scraped_data.get("systems", {}).items():
nid = sys_data.get("native_id")
if nid:
native_map[sys_id] = nid
root = Element("biosList")
systems = truth_data.get("systems", {})
for sys_id in sorted(systems):
sys_data = systems[sys_id]
files = sys_data.get("files", [])
if not files:
continue
native_id = native_map.get(sys_id, sys_id)
system_el = SubElement(root, "system", platform=native_id)
for fe in files:
name = fe.get("name", "")
if name.startswith("_"):
continue
dest = fe.get("destination", name)
md5 = fe.get("md5", "")
if isinstance(md5, list):
md5 = ",".join(md5)
required = fe.get("required", False)
attrs = {
"path": dest,
"md5": md5,
"mandatory": "true" if required else "false",
"hashMatchMandatory": "true" if required else "false",
}
SubElement(system_el, "bios", **attrs)
indent(root, space=" ")
tree = ElementTree(root)
tree.write(output_path, encoding="unicode", xml_declaration=True)
# Add trailing newline
with open(output_path, "a") as f:
f.write("\n")
def validate(self, truth_data: dict, output_path: str) -> list[str]:
from xml.etree.ElementTree import parse as xml_parse
tree = xml_parse(output_path)
root = tree.getroot()
exported_paths: set[str] = set()
for bios_el in root.iter("bios"):
path = bios_el.get("path", "")
if path:
exported_paths.add(path)
issues: list[str] = []
for sys_data in truth_data.get("systems", {}).values():
for fe in sys_data.get("files", []):
name = fe.get("name", "")
if name.startswith("_"):
continue
dest = fe.get("destination", name)
if dest not in exported_paths:
issues.append(f"missing: {dest}")
return issues

View File

@@ -0,0 +1,87 @@
"""Exporter for RetroBat batocera-systems.json format."""
from __future__ import annotations
import json
from pathlib import Path
from .base_exporter import BaseExporter
class Exporter(BaseExporter):
"""Export truth data to RetroBat batocera-systems.json format."""
@staticmethod
def platform_name() -> str:
return "retrobat"
def export(
self,
truth_data: dict,
output_path: str,
scraped_data: dict | None = None,
) -> None:
native_map: dict[str, str] = {}
if scraped_data:
for sys_id, sys_data in scraped_data.get("systems", {}).items():
nid = sys_data.get("native_id")
if nid:
native_map[sys_id] = nid
output: dict[str, dict] = {}
systems = truth_data.get("systems", {})
for sys_id in sorted(systems):
sys_data = systems[sys_id]
files = sys_data.get("files", [])
if not files:
continue
native_id = native_map.get(sys_id, sys_id)
bios_files: list[dict] = []
for fe in files:
name = fe.get("name", "")
if name.startswith("_"):
continue
dest = fe.get("destination", name)
md5 = fe.get("md5", "")
if isinstance(md5, list):
md5 = md5[0] if md5 else ""
entry = {"file": f"bios/{dest}"}
if md5:
entry["md5"] = md5
bios_files.append(entry)
if bios_files:
output[native_id] = {"biosFiles": bios_files}
Path(output_path).write_text(
json.dumps(output, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
def validate(self, truth_data: dict, output_path: str) -> list[str]:
data = json.loads(Path(output_path).read_text(encoding="utf-8"))
exported_files: set[str] = set()
for sys_data in data.values():
for bf in sys_data.get("biosFiles", []):
path = bf.get("file", "")
# Strip bios/ prefix, index both full path and basename
stripped = path.removeprefix("bios/")
exported_files.add(stripped)
basename = path.split("/")[-1] if "/" in path else path
exported_files.add(basename)
issues: list[str] = []
for sys_data in truth_data.get("systems", {}).values():
for fe in sys_data.get("files", []):
name = fe.get("name", "")
if name.startswith("_"):
continue
dest = fe.get("destination", name)
if name not in exported_files and dest not in exported_files:
issues.append(f"missing: {name}")
return issues