"""Regenerate the filament-id tables from Bambu's and OrcaSlicer's own data (#3387). Usage: python scripts/generate_filament_id_tables.py [BAMBU_PUBLIC_JSON] [ORCA_IDS_JSON] With no arguments both sources are downloaded: Bambu Cloud's public preset list (no login needed). Every filament entry carries its setting_id and the filament_id Bambu Studio sends to the printer for it. For most, dropping the "S" after "GF" turns one into the other; the presets where it does not (beyond SUNLU, which ``derived_filament_id`` handles by rule) go into SETTING_ID_FILAMENT_IDS, by full setting_id -- the suffix can decide it ("GFSA05_10" is Bambu PLA Matte, every other "GFSA05_nn" Bambu PLA Silk). A bare setting_id is listed only when the rule gives an id no preset has. Bambuddy itself builds bare ids from filament ids ("GFA05" -> "GFSA05"), so where the rule leads to a real filament ("GFSA05" -> Bambu PLA Silk), it wins over Bambu's legacy preset of that bare id ("GFSA05" is also Bambu PLA Matte @BBL X1): listing it would turn every such Silk spool into Matte. OrcaSlicer's resources/printers/bambu_filament_ids.json, the table Orca uses to translate its own filament ids ("OFMUWNkp") to Bambu's at the printer. It becomes ORCA_FILAMENT_IDS. Writes backend/app/utils/filament_id_tables.py and frontend/src/utils/filamentIdTables.ts (the frontend needs only the first table). Run it by hand when Bambu or Orca add filaments, and review the diff. """ import json import sys import urllib.request from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO_ROOT)) from backend.app.utils.filament_ids import derived_filament_id # noqa: E402 BAMBU_URL = "https://api.bambulab.com/v1/iot-service/api/slicer/setting?version=2.0.0.0&public=true" ORCA_URL = "https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer/main/resources/printers/bambu_filament_ids.json" BACKEND_OUT = REPO_ROOT / "backend" / "app" / "utils" / "filament_id_tables.py" FRONTEND_OUT = REPO_ROOT / "frontend" / "src" / "utils" / "filamentIdTables.ts" def _load(arg: str | None, url: str) -> dict: if arg: return json.loads(Path(arg).read_text()) request = urllib.request.Request(url, headers={"User-Agent": "bambuddy-filament-id-tables"}) with urllib.request.urlopen(request, timeout=30) as response: return json.load(response) def main() -> None: args = sys.argv[1:] bambu = _load(args[0] if args else None, BAMBU_URL) orca = _load(args[1] if len(args) > 1 else None, ORCA_URL) presets = (bambu.get("filament") or {}).get("public") or [] if not presets: sys.exit("Bambu's public list has no filament presets") real_ids = {preset.get("filament_id") for preset in presets} setting_ids: dict[str, str] = {} for preset in presets: setting_id, filament_id = preset.get("setting_id"), preset.get("filament_id") if not setting_id or not filament_id: continue derived = derived_filament_id(setting_id.split("_")[0]) if derived == filament_id or ("_" not in setting_id and derived in real_ids): continue setting_ids[setting_id] = filament_id orca_ids = { orca_id: entry["bambu_id"] for orca_id, entry in (orca.get("filaments") or {}).items() if isinstance(entry, dict) and entry.get("bambu_id") } if not orca_ids: sys.exit("Orca's table has no filaments") header = "Generated by scripts/generate_filament_id_tables.py -- do not edit by hand." lines = [f'"""{header}', "", f"{len(presets)} public Bambu filament presets read.", '"""', ""] lines.append("# Bambu setting_id -> filament_id, where the naming rule gives another id.") lines.append("SETTING_ID_FILAMENT_IDS: dict[str, str] = {") lines += [f' "{k}": "{v}",' for k, v in sorted(setting_ids.items())] lines.append("}") lines.append("") commit = orca.get("bambustudio_commit", "unknown") lines.append(f"# OrcaSlicer filament_id -> Bambu filament_id (Orca's table, Bambu Studio {commit}).") lines.append("ORCA_FILAMENT_IDS: dict[str, str] = {") lines += [f' "{k}": "{v}",' for k, v in sorted(orca_ids.items())] lines.append("}") BACKEND_OUT.write_text("\n".join(lines) + "\n") ts = [f"// {header}", "// Bambu setting_id -> filament_id, where the naming rule gives another id.", ""] ts.append("export const SETTING_ID_FILAMENT_IDS: Readonly> = {") ts += [f" {k}: '{v}'," for k, v in sorted(setting_ids.items())] ts.append("};") FRONTEND_OUT.write_text("\n".join(ts) + "\n") print(f"{len(setting_ids)} setting_id exceptions, {len(orca_ids)} Orca ids") if __name__ == "__main__": main()