diff --git a/scripts/export_native.py b/scripts/export_native.py index abfe1d58..b14affd9 100644 --- a/scripts/export_native.py +++ b/scripts/export_native.py @@ -16,6 +16,8 @@ from exporter import discover_exporters OUTPUT_FILENAMES: dict[str, str] = { "retroarch": "System.dat", + "lakka": "System.dat", + "retropie": "System.dat", "batocera": "batocera-systems", "recalbox": "es_bios.xml", "retrobat": "batocera-systems.json", @@ -25,9 +27,16 @@ OUTPUT_FILENAMES: dict[str, str] = { } -def output_filename(platform: str) -> str: - """Return the native output filename for a platform.""" - return OUTPUT_FILENAMES.get(platform, f"{platform}_bios.dat") +def output_path(platform: str, output_dir: str) -> str: + """Return the full output path for a platform's native export. + + 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( @@ -38,8 +47,6 @@ def run( ) -> int: """Export truth to native formats, return exit code.""" exporters = discover_exporters() - output_path = Path(output_dir) - output_path.mkdir(parents=True, exist_ok=True) errors = 0 @@ -63,7 +70,7 @@ def run( except (FileNotFoundError, OSError): pass - dest = str(output_path / output_filename(platform)) + dest = output_path(platform, output_dir) exporter = exporter_cls() exporter.export(truth_data, dest, scraped_data=scraped) diff --git a/scripts/exporter/base_exporter.py b/scripts/exporter/base_exporter.py index 5795ef22..9517f37a 100644 --- a/scripts/exporter/base_exporter.py +++ b/scripts/exporter/base_exporter.py @@ -31,6 +31,11 @@ class BaseExporter(ABC): """Check if a filename is a placeholder pattern (not a real file).""" 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 def _display_name( sys_id: str, scraped_sys: dict | None = None, diff --git a/scripts/exporter/batocera_exporter.py b/scripts/exporter/batocera_exporter.py index 745bb88d..b0ab90a4 100644 --- a/scripts/exporter/batocera_exporter.py +++ b/scripts/exporter/batocera_exporter.py @@ -53,7 +53,7 @@ class Exporter(BaseExporter): name = fe.get("name", "") if name.startswith("_") or self._is_pattern(name): continue - dest = fe.get("destination", name) + dest = self._dest(fe) md5 = fe.get("md5", "") if isinstance(md5, list): md5 = md5[0] if md5 else "" @@ -85,7 +85,7 @@ class Exporter(BaseExporter): name = fe.get("name", "") if name.startswith("_") or self._is_pattern(name): continue - dest = fe.get("destination", name) + dest = self._dest(fe) if dest not in content and name not in content: issues.append(f"missing: {name}") return issues diff --git a/scripts/exporter/emudeck_exporter.py b/scripts/exporter/emudeck_exporter.py index c26f584f..7e462c60 100644 --- a/scripts/exporter/emudeck_exporter.py +++ b/scripts/exporter/emudeck_exporter.py @@ -139,7 +139,7 @@ class Exporter(BaseExporter): else: # No MD5 hashes — check file existence for fe in sys_files[sys_id]: - dest = fe.get("destination", fe.get("name", "")) + dest = self._dest(fe) if dest: lines.append( f' if [ -f "$biosPath/{dest}" ]; then', diff --git a/scripts/exporter/recalbox_exporter.py b/scripts/exporter/recalbox_exporter.py index 9b5ae759..cfc94c00 100644 --- a/scripts/exporter/recalbox_exporter.py +++ b/scripts/exporter/recalbox_exporter.py @@ -61,7 +61,7 @@ class Exporter(BaseExporter): if name.startswith("_") or self._is_pattern(name): continue - dest = fe.get("destination", name) + dest = self._dest(fe) # Recalbox paths include system prefix path = f"{native_id}/{dest}" if "/" not in dest else dest @@ -113,7 +113,7 @@ class Exporter(BaseExporter): name = fe.get("name", "") if name.startswith("_") or self._is_pattern(name): continue - dest = fe.get("destination", name) + dest = self._dest(fe) if name not in exported_paths and dest not in exported_paths: issues.append(f"missing: {name}") return issues diff --git a/scripts/exporter/retrobat_exporter.py b/scripts/exporter/retrobat_exporter.py index 814dde37..37d88068 100644 --- a/scripts/exporter/retrobat_exporter.py +++ b/scripts/exporter/retrobat_exporter.py @@ -55,7 +55,7 @@ class Exporter(BaseExporter): name = fe.get("name", "") if name.startswith("_") or self._is_pattern(name): continue - dest = fe.get("destination", name) + dest = self._dest(fe) md5 = fe.get("md5", "") if isinstance(md5, list): md5 = md5[0] if md5 else "" @@ -68,10 +68,16 @@ class Exporter(BaseExporter): bios_files.append(entry) if bios_files: - sys_entry: OrderedDict[str, object] = OrderedDict() - sys_entry["name"] = display_name - sys_entry["biosFiles"] = bios_files - output[native_id] = sys_entry + if native_id in output: + existing_files = {e.get("file") for e in output[native_id]["biosFiles"]} + for entry in bios_files: + 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( json.dumps(output, indent=2, ensure_ascii=False) + "\n", @@ -96,7 +102,7 @@ class Exporter(BaseExporter): name = fe.get("name", "") if name.startswith("_") or self._is_pattern(name): continue - dest = fe.get("destination", name) + dest = self._dest(fe) if name not in exported_files and dest not in exported_files: issues.append(f"missing: {name}") return issues diff --git a/scripts/exporter/retrodeck_exporter.py b/scripts/exporter/retrodeck_exporter.py index 5887919b..269867ad 100644 --- a/scripts/exporter/retrodeck_exporter.py +++ b/scripts/exporter/retrodeck_exporter.py @@ -138,7 +138,7 @@ class Exporter(BaseExporter): if name.startswith("_") or self._is_pattern(name): continue - dest = fe.get("destination", name) + dest = self._dest(fe) path_token = _dest_to_path_token(dest) md5 = fe.get("md5", "") @@ -167,10 +167,18 @@ class Exporter(BaseExporter): bios_entries.append(entry) if bios_entries: - component = OrderedDict() - component["system"] = native_id - component["bios"] = bios_entries - manifest[native_id] = component + if native_id in manifest: + # Merge into existing component (multiple truth systems + # may map to the same native ID) + 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( json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",