fix: exporters use _dest fallback, merge colliding systems, per-platform subdirs

This commit is contained in:
Abdessamad Derraz
2026-03-30 17:15:44 +02:00
parent 0be68edad0
commit 4fbb3571f8
7 changed files with 48 additions and 22 deletions

View File

@@ -16,6 +16,8 @@ from exporter import discover_exporters
OUTPUT_FILENAMES: dict[str, str] = { OUTPUT_FILENAMES: dict[str, str] = {
"retroarch": "System.dat", "retroarch": "System.dat",
"lakka": "System.dat",
"retropie": "System.dat",
"batocera": "batocera-systems", "batocera": "batocera-systems",
"recalbox": "es_bios.xml", "recalbox": "es_bios.xml",
"retrobat": "batocera-systems.json", "retrobat": "batocera-systems.json",
@@ -25,9 +27,16 @@ OUTPUT_FILENAMES: dict[str, str] = {
} }
def output_filename(platform: str) -> str: def output_path(platform: str, output_dir: str) -> str:
"""Return the native output filename for a platform.""" """Return the full output path for a platform's native export.
return OUTPUT_FILENAMES.get(platform, f"{platform}_bios.dat")
Each platform gets its own subdirectory to avoid filename collisions
(e.g. retroarch, lakka, retropie all produce System.dat).
"""
filename = OUTPUT_FILENAMES.get(platform, f"{platform}_bios.dat")
plat_dir = Path(output_dir) / platform
plat_dir.mkdir(parents=True, exist_ok=True)
return str(plat_dir / filename)
def run( def run(
@@ -38,8 +47,6 @@ def run(
) -> int: ) -> int:
"""Export truth to native formats, return exit code.""" """Export truth to native formats, return exit code."""
exporters = discover_exporters() exporters = discover_exporters()
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
errors = 0 errors = 0
@@ -63,7 +70,7 @@ def run(
except (FileNotFoundError, OSError): except (FileNotFoundError, OSError):
pass pass
dest = str(output_path / output_filename(platform)) dest = output_path(platform, output_dir)
exporter = exporter_cls() exporter = exporter_cls()
exporter.export(truth_data, dest, scraped_data=scraped) exporter.export(truth_data, dest, scraped_data=scraped)

View File

@@ -31,6 +31,11 @@ class BaseExporter(ABC):
"""Check if a filename is a placeholder pattern (not a real file).""" """Check if a filename is a placeholder pattern (not a real file)."""
return "<" in name or ">" in name or "*" in name return "<" in name or ">" in name or "*" in name
@staticmethod
def _dest(fe: dict) -> str:
"""Get destination path for a file entry, falling back to name."""
return fe.get("path") or fe.get("destination") or fe.get("name", "")
@staticmethod @staticmethod
def _display_name( def _display_name(
sys_id: str, scraped_sys: dict | None = None, sys_id: str, scraped_sys: dict | None = None,

View File

@@ -53,7 +53,7 @@ class Exporter(BaseExporter):
name = fe.get("name", "") name = fe.get("name", "")
if name.startswith("_") or self._is_pattern(name): if name.startswith("_") or self._is_pattern(name):
continue continue
dest = fe.get("destination", name) dest = self._dest(fe)
md5 = fe.get("md5", "") md5 = fe.get("md5", "")
if isinstance(md5, list): if isinstance(md5, list):
md5 = md5[0] if md5 else "" md5 = md5[0] if md5 else ""
@@ -85,7 +85,7 @@ class Exporter(BaseExporter):
name = fe.get("name", "") name = fe.get("name", "")
if name.startswith("_") or self._is_pattern(name): if name.startswith("_") or self._is_pattern(name):
continue continue
dest = fe.get("destination", name) dest = self._dest(fe)
if dest not in content and name not in content: if dest not in content and name not in content:
issues.append(f"missing: {name}") issues.append(f"missing: {name}")
return issues return issues

View File

@@ -139,7 +139,7 @@ class Exporter(BaseExporter):
else: else:
# No MD5 hashes — check file existence # No MD5 hashes — check file existence
for fe in sys_files[sys_id]: for fe in sys_files[sys_id]:
dest = fe.get("destination", fe.get("name", "")) dest = self._dest(fe)
if dest: if dest:
lines.append( lines.append(
f' if [ -f "$biosPath/{dest}" ]; then', f' if [ -f "$biosPath/{dest}" ]; then',

View File

@@ -61,7 +61,7 @@ class Exporter(BaseExporter):
if name.startswith("_") or self._is_pattern(name): if name.startswith("_") or self._is_pattern(name):
continue continue
dest = fe.get("destination", name) dest = self._dest(fe)
# Recalbox paths include system prefix # Recalbox paths include system prefix
path = f"{native_id}/{dest}" if "/" not in dest else dest path = f"{native_id}/{dest}" if "/" not in dest else dest
@@ -113,7 +113,7 @@ class Exporter(BaseExporter):
name = fe.get("name", "") name = fe.get("name", "")
if name.startswith("_") or self._is_pattern(name): if name.startswith("_") or self._is_pattern(name):
continue continue
dest = fe.get("destination", name) dest = self._dest(fe)
if name not in exported_paths and dest not in exported_paths: if name not in exported_paths and dest not in exported_paths:
issues.append(f"missing: {name}") issues.append(f"missing: {name}")
return issues return issues

View File

@@ -55,7 +55,7 @@ class Exporter(BaseExporter):
name = fe.get("name", "") name = fe.get("name", "")
if name.startswith("_") or self._is_pattern(name): if name.startswith("_") or self._is_pattern(name):
continue continue
dest = fe.get("destination", name) dest = self._dest(fe)
md5 = fe.get("md5", "") md5 = fe.get("md5", "")
if isinstance(md5, list): if isinstance(md5, list):
md5 = md5[0] if md5 else "" md5 = md5[0] if md5 else ""
@@ -68,10 +68,16 @@ class Exporter(BaseExporter):
bios_files.append(entry) bios_files.append(entry)
if bios_files: if bios_files:
sys_entry: OrderedDict[str, object] = OrderedDict() if native_id in output:
sys_entry["name"] = display_name existing_files = {e.get("file") for e in output[native_id]["biosFiles"]}
sys_entry["biosFiles"] = bios_files for entry in bios_files:
output[native_id] = sys_entry if entry.get("file") not in existing_files:
output[native_id]["biosFiles"].append(entry)
else:
sys_entry: OrderedDict[str, object] = OrderedDict()
sys_entry["name"] = display_name
sys_entry["biosFiles"] = bios_files
output[native_id] = sys_entry
Path(output_path).write_text( Path(output_path).write_text(
json.dumps(output, indent=2, ensure_ascii=False) + "\n", json.dumps(output, indent=2, ensure_ascii=False) + "\n",
@@ -96,7 +102,7 @@ class Exporter(BaseExporter):
name = fe.get("name", "") name = fe.get("name", "")
if name.startswith("_") or self._is_pattern(name): if name.startswith("_") or self._is_pattern(name):
continue continue
dest = fe.get("destination", name) dest = self._dest(fe)
if name not in exported_files and dest not in exported_files: if name not in exported_files and dest not in exported_files:
issues.append(f"missing: {name}") issues.append(f"missing: {name}")
return issues return issues

View File

@@ -138,7 +138,7 @@ class Exporter(BaseExporter):
if name.startswith("_") or self._is_pattern(name): if name.startswith("_") or self._is_pattern(name):
continue continue
dest = fe.get("destination", name) dest = self._dest(fe)
path_token = _dest_to_path_token(dest) path_token = _dest_to_path_token(dest)
md5 = fe.get("md5", "") md5 = fe.get("md5", "")
@@ -167,10 +167,18 @@ class Exporter(BaseExporter):
bios_entries.append(entry) bios_entries.append(entry)
if bios_entries: if bios_entries:
component = OrderedDict() if native_id in manifest:
component["system"] = native_id # Merge into existing component (multiple truth systems
component["bios"] = bios_entries # may map to the same native ID)
manifest[native_id] = component existing_names = {e["filename"] for e in manifest[native_id]["bios"]}
for entry in bios_entries:
if entry["filename"] not in existing_names:
manifest[native_id]["bios"].append(entry)
else:
component = OrderedDict()
component["system"] = native_id
component["bios"] = bios_entries
manifest[native_id] = component
Path(output_path).write_text( Path(output_path).write_text(
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",