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 legacy ``content_kind`` (``\"presets\"`` / ``\"sequences\"``) is kept for older data; zones may hold both preset tiles and ``sequence_ids``. """ 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): """No-op: presets and sequences may coexist on one zone.""" _ = doc 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 {} self[id_str].update(patch) 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())