- Optional profile_id on groups; UI and API for shared vs profile-only groups\n- Zone content_kind (presets vs sequences); edit modal shows matching sections; devices via groups only\n- Server sequence playback folds zone brightness into preset wire b (per MAC where needed)\n- Related preset/sequence/audio/beat-route and client updates Co-authored-by: Cursor <cursoragent@cursor.com>
160 lines
5.8 KiB
Python
160 lines
5.8 KiB
Python
from models.model import Model
|
|
|
|
|
|
class Sequence(Model):
|
|
def load(self):
|
|
super().load()
|
|
self._migrate_after_load()
|
|
|
|
def _migrate_after_load(self):
|
|
try:
|
|
from models.profile import Profile
|
|
|
|
profiles = Profile()
|
|
profile_list = profiles.list()
|
|
default_profile_id = profile_list[0] if profile_list else None
|
|
except Exception:
|
|
default_profile_id = None
|
|
|
|
changed = False
|
|
for _sid, doc in list(self.items()):
|
|
if not isinstance(doc, dict):
|
|
continue
|
|
if not isinstance(doc.get("steps"), list):
|
|
presets = doc.get("presets")
|
|
if isinstance(presets, list) and presets:
|
|
doc["steps"] = [
|
|
{"preset_id": str(p), "group_ids": []} for p in presets
|
|
]
|
|
else:
|
|
doc["steps"] = []
|
|
changed = True
|
|
if "step_duration_ms" not in doc:
|
|
dur = doc.get("sequence_duration")
|
|
doc["step_duration_ms"] = (
|
|
int(dur) if isinstance(dur, (int, float)) else 3000
|
|
)
|
|
changed = True
|
|
if "loop" not in doc:
|
|
doc["loop"] = bool(doc.get("sequence_loop", False))
|
|
changed = True
|
|
if "name" not in doc:
|
|
doc["name"] = str(doc.get("group_name") or "")
|
|
changed = True
|
|
if "profile_id" not in doc and default_profile_id is not None:
|
|
doc["profile_id"] = str(default_profile_id)
|
|
changed = True
|
|
if not isinstance(doc.get("lanes"), list):
|
|
steps = doc.get("steps")
|
|
if isinstance(steps, list) and steps:
|
|
doc["lanes"] = [list(steps)]
|
|
else:
|
|
doc["lanes"] = [[]]
|
|
changed = True
|
|
if "group_ids" not in doc or not isinstance(doc.get("group_ids"), list):
|
|
doc["group_ids"] = []
|
|
changed = True
|
|
if doc.get("advance_mode") != "beats":
|
|
doc["advance_mode"] = "beats"
|
|
changed = True
|
|
if "simulated_bpm" not in doc:
|
|
doc["simulated_bpm"] = 120
|
|
changed = True
|
|
else:
|
|
try:
|
|
sb = int(float(doc["simulated_bpm"]))
|
|
doc["simulated_bpm"] = max(30, min(300, sb))
|
|
except (TypeError, ValueError):
|
|
doc["simulated_bpm"] = 120
|
|
changed = True
|
|
if "sequence_transition" not in doc:
|
|
doc["sequence_transition"] = 500
|
|
changed = True
|
|
# Ensure each step has beats (beat-based advance); default 1
|
|
for lane in doc.get("lanes") or []:
|
|
if not isinstance(lane, list):
|
|
continue
|
|
for step in lane:
|
|
if not isinstance(step, dict):
|
|
continue
|
|
if "beats" not in step:
|
|
step["beats"] = 1
|
|
changed = True
|
|
# Per-lane group ids (parallel to ``lanes``)
|
|
lanes_list = [x for x in (doc.get("lanes") or []) if isinstance(x, list)]
|
|
n_lanes = len(lanes_list)
|
|
lg = doc.get("lanes_group_ids")
|
|
if n_lanes and (not isinstance(lg, list) or len(lg) != n_lanes):
|
|
shared = doc.get("group_ids") if isinstance(doc.get("group_ids"), list) else []
|
|
shared_s = [str(x).strip() for x in shared if x is not None and str(x).strip()]
|
|
if n_lanes == 1 and lanes_list[0]:
|
|
first = lanes_list[0][0] if isinstance(lanes_list[0][0], dict) else {}
|
|
step_g = (
|
|
first.get("group_ids")
|
|
if isinstance(first.get("group_ids"), list)
|
|
else []
|
|
)
|
|
step_s = [
|
|
str(x).strip() for x in step_g if x is not None and str(x).strip()
|
|
]
|
|
doc["lanes_group_ids"] = [step_s if step_s else list(shared_s)]
|
|
else:
|
|
doc["lanes_group_ids"] = [list(shared_s) for _ in range(n_lanes)]
|
|
changed = True
|
|
if changed:
|
|
self.save()
|
|
|
|
def create(self, profile_id=None):
|
|
next_id = self.get_next_id()
|
|
self[next_id] = {
|
|
"name": "",
|
|
"profile_id": str(profile_id) if profile_id is not None else None,
|
|
"group_ids": [],
|
|
"lanes": [[]],
|
|
"lanes_group_ids": [[]],
|
|
"advance_mode": "beats",
|
|
"steps": [],
|
|
"step_duration_ms": 3000,
|
|
"simulated_bpm": 120,
|
|
"sequence_transition": 500,
|
|
"loop": True,
|
|
}
|
|
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
|
|
if not isinstance(data, dict):
|
|
return False
|
|
data = dict(data)
|
|
steps = data.get("steps")
|
|
lanes = data.get("lanes")
|
|
if isinstance(steps, list) and steps:
|
|
lanes_ok = (
|
|
isinstance(lanes, list)
|
|
and lanes
|
|
and any(isinstance(x, list) and len(x) > 0 for x in lanes)
|
|
)
|
|
if not lanes_ok:
|
|
data["lanes"] = [list(steps)]
|
|
self[id_str].update(data)
|
|
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())
|