"""Exporter for libretro System.dat (clrmamepro DAT format). Produces a single 'game' block with all ROMs grouped by system, matching the exact format of libretro-database/dat/System.dat. """ from __future__ import annotations import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from scraper.dat_parser import parse_dat from .base_exporter import BaseExporter def _slug_to_native(slug: str) -> str: """Convert a system slug to 'Manufacturer - Console' format.""" parts = slug.split("-", 1) if len(parts) == 1: return parts[0].title() manufacturer = parts[0].replace("-", " ").title() console = parts[1].replace("-", " ").title() return f"{manufacturer} - {console}" class Exporter(BaseExporter): """Export truth data to libretro System.dat format.""" @staticmethod def platform_name() -> str: return "retroarch" 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 # Match exact header format of libretro-database/dat/System.dat version = "" if scraped_data: version = scraped_data.get("dat_version", scraped_data.get("version", "")) lines: list[str] = [ "clrmamepro (", '\tname "System"', '\tdescription "System"', '\tcomment "System, firmware, and BIOS files used by libretro cores."', ] if version: lines.append(f"\tversion {version}") lines.extend( [ '\tauthor "libretro"', '\thomepage "https://github.com/libretro/libretro-database/blob/master/dat/System.dat"', '\turl "https://raw.githubusercontent.com/libretro/libretro-database/master/dat/System.dat"', ")", "", "game (", '\tname "System"', '\tcomment "System"', ] ) 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_name = native_map.get(sys_id, _slug_to_native(sys_id)) lines.append("") lines.append(f'\tcomment "{native_name}"') for fe in files: name = fe.get("name", "") if name.startswith("_") or self._is_pattern(name): continue # Quote names with spaces or special chars (matching original format) needs_quote = " " in name or "(" in name or ")" in name name_str = f'"{name}"' if needs_quote else name rom_parts = [f"name {name_str}"] size = fe.get("size") if size: rom_parts.append(f"size {size}") crc = fe.get("crc32", "") if crc: rom_parts.append(f"crc {crc.upper()}") md5 = fe.get("md5", "") if isinstance(md5, list): md5 = md5[0] if md5 else "" if md5: rom_parts.append(f"md5 {md5}") sha1 = fe.get("sha1", "") if isinstance(sha1, list): sha1 = sha1[0] if sha1 else "" if sha1: rom_parts.append(f"sha1 {sha1}") lines.append(f"\trom ( {' '.join(rom_parts)} )") 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") parsed = parse_dat(content) exported_names: set[str] = set() for rom in parsed: exported_names.add(rom.name) 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("_") or self._is_pattern(name): continue if name not in exported_names: issues.append(f"missing: {name}") return issues