import os import shutil from models.model import Model def _maybe_migrate_tab_json_to_zone(): """One-time copy ``db/tab.json`` → ``db/zone.json`` when upgrading.""" try: base = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) db_dir = os.path.join(base, "db") zone_path = os.path.join(db_dir, "zone.json") tab_path = os.path.join(db_dir, "tab.json") if not os.path.exists(zone_path) and os.path.exists(tab_path): shutil.copy2(tab_path, zone_path) print("Migrated db/tab.json -> db/zone.json") except OSError: pass class Zone(Model): """Preset layout row (stored in ``db/zone.json``); legacy storage was ``tab.json`` / Tab. Optional ``content_kind`` on a row: ``\"presets\"`` (preset tiles only) or ``\"sequences\"`` (sequence tiles only). Legacy rows without ``content_kind`` are inferred on load. """ def __init__(self): if not getattr(Zone, "_migration_checked", False): _maybe_migrate_tab_json_to_zone() Zone._migration_checked = True super().__init__() def load(self): super().load() changed = False for zid, doc in list(self.items()): if not isinstance(doc, dict): continue if "group_ids" not in doc: doc["group_ids"] = [] changed = True if "preset_group_ids" not in doc or not isinstance(doc.get("preset_group_ids"), dict): doc["preset_group_ids"] = {} changed = True if "sequence_ids" not in doc or not isinstance(doc.get("sequence_ids"), list): doc["sequence_ids"] = [] changed = True if not self._normalized_content_kind(doc): doc["content_kind"] = self._infer_content_kind(doc) changed = True if changed: self.save() @staticmethod def _normalized_content_kind(doc): if not isinstance(doc, dict): return None kind = doc.get("content_kind") return kind if kind in ("presets", "sequences") else None @staticmethod def _preset_ids_in_doc(doc): if not isinstance(doc, dict): return [] flat = doc.get("presets_flat") if isinstance(flat, list): return [str(x) for x in flat if x is not None and str(x).strip()] presets = doc.get("presets") if not isinstance(presets, list) or not presets: return [] if isinstance(presets[0], str): return [str(x) for x in presets if x is not None and str(x).strip()] if isinstance(presets[0], list): out = [] for row in presets: if isinstance(row, list): out.extend(str(x) for x in row if x is not None and str(x).strip()) return out return [] @classmethod def _infer_content_kind(cls, doc): kind = cls._normalized_content_kind(doc) if kind: return kind seq_ids = [ str(x).strip() for x in (doc.get("sequence_ids") or []) if x is not None and str(x).strip() ] preset_ids = cls._preset_ids_in_doc(doc) if seq_ids and not preset_ids: return "sequences" return "presets" def _enforce_content_kind_invariants(self, doc): """Presets-only zones hold no sequences; sequences-only hold no preset tiles.""" kind = self._normalized_content_kind(doc) if kind == "presets": doc["sequence_ids"] = [] elif kind == "sequences": doc["presets"] = [] doc["presets_flat"] = [] def create(self, name="", names=None, presets=None, group_ids=None, content_kind=None): next_id = self.get_next_id() gid_list = [] if isinstance(group_ids, list): gid_list = [str(x).strip() for x in group_ids if x is not None and str(x).strip()] doc = { "name": name, "names": names if names else [], "group_ids": gid_list, "preset_group_ids": {}, "presets": presets if presets else [], "default_preset": None, "brightness": 255, } if content_kind in ("presets", "sequences"): doc["content_kind"] = content_kind if "sequence_ids" not in doc: doc["sequence_ids"] = [] self._enforce_content_kind_invariants(doc) self[next_id] = doc self.save() return next_id def read(self, id): id_str = str(id) return self.get(id_str, None) def update(self, id, data): id_str = str(id) if id_str not in self: return False patch = dict(data) if isinstance(data, dict) else {} doc = self[id_str] locked_kind = self._normalized_content_kind(doc) or self._infer_content_kind(doc) if "content_kind" in patch: patch["content_kind"] = locked_kind self[id_str].update(patch) if "content_kind" in patch: self._enforce_content_kind_invariants(self[id_str]) self.save() return True def delete(self, id): id_str = str(id) if id_str not in self: return False self.pop(id_str) self.save() return True def list(self): return list(self.keys())