- Add sequence_playback with beat and time advance, zone targeting fixes - Per-lane manual beat routing in beat_driver_route (parallel lanes) - Sequence API, editor JS, fix sequence model filename, tests Co-authored-by: Cursor <cursoragent@cursor.com>
208 lines
7.0 KiB
Python
208 lines
7.0 KiB
Python
from microdot import Microdot
|
|
from microdot.session import with_session
|
|
from models.sequence import Sequence
|
|
from models.profile import Profile
|
|
from models.transport import get_current_sender
|
|
import json
|
|
|
|
controller = Microdot()
|
|
sequences = Sequence()
|
|
profiles = Profile()
|
|
|
|
|
|
def get_current_profile_id(session=None):
|
|
"""Get the current active profile ID from session or fallback to first."""
|
|
profile_list = profiles.list()
|
|
session_profile = None
|
|
if session is not None:
|
|
session_profile = session.get("current_profile")
|
|
if session_profile and session_profile in profile_list:
|
|
return session_profile
|
|
if profile_list:
|
|
return profile_list[0]
|
|
return None
|
|
|
|
|
|
@controller.get("")
|
|
@with_session
|
|
async def list_sequences(request, session):
|
|
"""List sequences for the current profile."""
|
|
current_profile_id = get_current_profile_id(session)
|
|
if not current_profile_id:
|
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
|
scoped = {
|
|
sid: sdata
|
|
for sid, sdata in sequences.items()
|
|
if isinstance(sdata, dict)
|
|
and str(sdata.get("profile_id")) == str(current_profile_id)
|
|
}
|
|
return json.dumps(scoped), 200, {"Content-Type": "application/json"}
|
|
|
|
|
|
@controller.get("/<id>")
|
|
@with_session
|
|
async def get_sequence(request, session, id):
|
|
"""Get a specific sequence by ID (current profile only)."""
|
|
current_profile_id = get_current_profile_id(session)
|
|
seq = sequences.read(id)
|
|
if (
|
|
seq
|
|
and current_profile_id
|
|
and str(seq.get("profile_id")) == str(current_profile_id)
|
|
):
|
|
return json.dumps(seq), 200, {"Content-Type": "application/json"}
|
|
return json.dumps({"error": "Sequence not found"}), 404
|
|
|
|
|
|
@controller.post("")
|
|
@with_session
|
|
async def create_sequence(request, session):
|
|
"""Create a new sequence for the current profile."""
|
|
try:
|
|
try:
|
|
data = request.json or {}
|
|
except Exception:
|
|
return (
|
|
json.dumps({"error": "Invalid JSON"}),
|
|
400,
|
|
{"Content-Type": "application/json"},
|
|
)
|
|
current_profile_id = get_current_profile_id(session)
|
|
if not current_profile_id:
|
|
return (
|
|
json.dumps({"error": "No profile available"}),
|
|
404,
|
|
{"Content-Type": "application/json"},
|
|
)
|
|
sequence_id = sequences.create(current_profile_id)
|
|
if not isinstance(data, dict):
|
|
data = {}
|
|
data = dict(data)
|
|
data["profile_id"] = str(current_profile_id)
|
|
if sequences.update(sequence_id, data):
|
|
seq_data = sequences.read(sequence_id)
|
|
return (
|
|
json.dumps({sequence_id: seq_data}),
|
|
201,
|
|
{"Content-Type": "application/json"},
|
|
)
|
|
return (
|
|
json.dumps({"error": "Failed to create sequence"}),
|
|
400,
|
|
{"Content-Type": "application/json"},
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)}), 400, {"Content-Type": "application/json"}
|
|
|
|
|
|
@controller.put("/<id>")
|
|
@with_session
|
|
async def update_sequence(request, session, id):
|
|
"""Update an existing sequence (current profile only)."""
|
|
try:
|
|
current_profile_id = get_current_profile_id(session)
|
|
seq = sequences.read(id)
|
|
if not seq or str(seq.get("profile_id")) != str(current_profile_id):
|
|
return json.dumps({"error": "Sequence not found"}), 404
|
|
data = request.json
|
|
if not isinstance(data, dict):
|
|
return (
|
|
json.dumps({"error": "Invalid JSON"}),
|
|
400,
|
|
{"Content-Type": "application/json"},
|
|
)
|
|
data = dict(data)
|
|
data["profile_id"] = str(current_profile_id)
|
|
if sequences.update(id, data):
|
|
try:
|
|
from util.sequence_playback import stop_if_playing_sequence
|
|
|
|
stop_if_playing_sequence(str(id))
|
|
except Exception:
|
|
pass
|
|
return json.dumps(sequences.read(id)), 200, {"Content-Type": "application/json"}
|
|
return json.dumps({"error": "Sequence not found"}), 404
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)}), 400, {"Content-Type": "application/json"}
|
|
|
|
|
|
@controller.delete("/<id>")
|
|
@with_session
|
|
async def delete_sequence(request, session, id):
|
|
"""Delete a sequence (current profile only)."""
|
|
current_profile_id = get_current_profile_id(session)
|
|
seq = sequences.read(id)
|
|
if not seq or str(seq.get("profile_id")) != str(current_profile_id):
|
|
return json.dumps({"error": "Sequence not found"}), 404
|
|
try:
|
|
from util.sequence_playback import stop_if_playing_sequence
|
|
|
|
stop_if_playing_sequence(str(id))
|
|
except Exception:
|
|
pass
|
|
if sequences.delete(id):
|
|
return (
|
|
json.dumps({"message": "Sequence deleted successfully"}),
|
|
200,
|
|
{"Content-Type": "application/json"},
|
|
)
|
|
return json.dumps({"error": "Sequence not found"}), 404
|
|
|
|
|
|
@controller.post("/stop")
|
|
@with_session
|
|
async def stop_sequence_playback(request, session):
|
|
"""Stop server-driven zone sequence playback."""
|
|
_ = request
|
|
try:
|
|
from util.sequence_playback import stop
|
|
|
|
stop()
|
|
return json.dumps({"ok": True}), 200, {"Content-Type": "application/json"}
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)}), 500, {"Content-Type": "application/json"}
|
|
|
|
|
|
@controller.post("/<id>/play")
|
|
@with_session
|
|
async def play_sequence(request, session, id):
|
|
"""Start server-driven playback for a sequence in a zone (body: {\"zone_id\": \"...\"})."""
|
|
if not get_current_sender():
|
|
return (
|
|
json.dumps({"error": "Transport not configured"}),
|
|
503,
|
|
{"Content-Type": "application/json"},
|
|
)
|
|
current_profile_id = get_current_profile_id(session)
|
|
if not current_profile_id:
|
|
return (
|
|
json.dumps({"error": "No profile available"}),
|
|
404,
|
|
{"Content-Type": "application/json"},
|
|
)
|
|
try:
|
|
data = request.json or {}
|
|
except Exception:
|
|
data = {}
|
|
if not isinstance(data, dict):
|
|
data = {}
|
|
zone_id = data.get("zone_id") or data.get("zoneId")
|
|
if zone_id is None or str(zone_id).strip() == "":
|
|
return (
|
|
json.dumps({"error": "zone_id required"}),
|
|
400,
|
|
{"Content-Type": "application/json"},
|
|
)
|
|
zone_id = str(zone_id).strip()
|
|
try:
|
|
from util.sequence_playback import start
|
|
|
|
await start(zone_id, str(id), str(current_profile_id))
|
|
return json.dumps({"ok": True}), 200, {"Content-Type": "application/json"}
|
|
except ValueError as e:
|
|
return json.dumps({"error": str(e)}), 400, {"Content-Type": "application/json"}
|
|
except RuntimeError as e:
|
|
return json.dumps({"error": str(e)}), 503, {"Content-Type": "application/json"}
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)}), 500, {"Content-Type": "application/json"}
|