391 lines
14 KiB
Python
391 lines
14 KiB
Python
from microdot import Microdot
|
|
from microdot.session import with_session
|
|
from models.profile import Profile
|
|
from models.zone import Zone
|
|
from models.preset import Preset
|
|
import json
|
|
|
|
controller = Microdot()
|
|
profiles = Profile()
|
|
zones = Zone()
|
|
presets = Preset()
|
|
|
|
@controller.get('')
|
|
@with_session
|
|
async def list_profiles(request, session):
|
|
"""List all profiles with current profile info."""
|
|
profile_list = profiles.list()
|
|
current_id = session.get('current_profile')
|
|
if current_id and current_id not in profile_list:
|
|
current_id = None
|
|
|
|
# If no current profile in session, use first one
|
|
if not current_id and profile_list:
|
|
current_id = profile_list[0]
|
|
session['current_profile'] = str(current_id)
|
|
session.save()
|
|
|
|
# Build profiles object
|
|
profiles_data = {}
|
|
for profile_id in profile_list:
|
|
profile_data = profiles.read(profile_id)
|
|
if profile_data:
|
|
profiles_data[profile_id] = profile_data
|
|
|
|
return json.dumps({
|
|
"profiles": profiles_data,
|
|
"current_profile_id": current_id
|
|
}), 200, {'Content-Type': 'application/json'}
|
|
|
|
@controller.get('/current')
|
|
@with_session
|
|
async def get_current_profile(request, session):
|
|
"""Get the current profile ID from session (or fallback)."""
|
|
profile_list = profiles.list()
|
|
current_id = session.get('current_profile')
|
|
if current_id and current_id not in profile_list:
|
|
current_id = None
|
|
if not current_id and profile_list:
|
|
current_id = profile_list[0]
|
|
session['current_profile'] = str(current_id)
|
|
session.save()
|
|
if current_id:
|
|
profile = profiles.read(current_id)
|
|
return json.dumps({"id": current_id, "profile": profile}), 200, {'Content-Type': 'application/json'}
|
|
return json.dumps({"error": "No profile available"}), 404
|
|
|
|
@controller.get('/<id>')
|
|
@with_session
|
|
async def get_profile(request, id, session):
|
|
"""Get a specific profile by ID."""
|
|
# Handle 'current' as a special case
|
|
if id == 'current':
|
|
return await get_current_profile(request, session)
|
|
|
|
profile = profiles.read(id)
|
|
if profile:
|
|
return json.dumps(profile), 200, {'Content-Type': 'application/json'}
|
|
return json.dumps({"error": "Profile not found"}), 404
|
|
|
|
@controller.post('/<id>/apply')
|
|
@with_session
|
|
async def apply_profile(request, session, id):
|
|
"""Apply a profile by saving it to session."""
|
|
if not profiles.read(id):
|
|
return json.dumps({"error": "Profile not found"}), 404
|
|
session['current_profile'] = str(id)
|
|
session.save()
|
|
return json.dumps({"message": "Profile applied", "id": str(id)}), 200, {'Content-Type': 'application/json'}
|
|
|
|
@controller.post('')
|
|
async def create_profile(request):
|
|
"""Create a new profile."""
|
|
try:
|
|
data = dict(request.json or {})
|
|
name = data.get("name", "")
|
|
seed_raw = data.get("seed_dj_zone", False)
|
|
if isinstance(seed_raw, str):
|
|
seed_dj_zone = seed_raw.strip().lower() in ("1", "true", "yes", "on")
|
|
else:
|
|
seed_dj_zone = bool(seed_raw)
|
|
# Request-only flag: do not persist on profile records.
|
|
data.pop("seed_dj_zone", None)
|
|
profile_id = profiles.create(name)
|
|
# Avoid persisting request-only fields.
|
|
data.pop("name", None)
|
|
if data:
|
|
profiles.update(profile_id, data)
|
|
|
|
# New profiles always start with a default zone pre-populated with starter presets.
|
|
default_preset_ids = []
|
|
default_preset_defs = [
|
|
{
|
|
"name": "on",
|
|
"pattern": "on",
|
|
"colors": ["#FFFFFF"],
|
|
"brightness": 255,
|
|
"delay": 100,
|
|
"auto": True,
|
|
},
|
|
{
|
|
"name": "off",
|
|
"pattern": "off",
|
|
"colors": [],
|
|
"brightness": 0,
|
|
"delay": 100,
|
|
"auto": True,
|
|
},
|
|
{
|
|
"name": "rainbow",
|
|
"pattern": "rainbow",
|
|
"colors": [],
|
|
"brightness": 255,
|
|
"delay": 100,
|
|
"auto": True,
|
|
"n1": 2,
|
|
},
|
|
{
|
|
"name": "Colour Cycle",
|
|
"pattern": "colour_cycle",
|
|
"colors": ["#FF0000", "#00FF00", "#0000FF"],
|
|
"brightness": 255,
|
|
"delay": 100,
|
|
"auto": True,
|
|
"n1": 1,
|
|
},
|
|
{
|
|
"name": "transition",
|
|
"pattern": "transition",
|
|
"colors": ["#FF0000", "#00FF00", "#0000FF"],
|
|
"brightness": 255,
|
|
"delay": 500,
|
|
"auto": True,
|
|
},
|
|
{
|
|
"name": "flicker",
|
|
"pattern": "flicker",
|
|
"colors": ["#FFB84D"],
|
|
"brightness": 255,
|
|
"delay": 80,
|
|
"auto": True,
|
|
"n1": 30,
|
|
},
|
|
{
|
|
"name": "flame",
|
|
"pattern": "flame",
|
|
"colors": [],
|
|
"brightness": 255,
|
|
"delay": 50,
|
|
"auto": True,
|
|
"n1": 35,
|
|
"n2": 2600,
|
|
"n3": 0,
|
|
"n4": 0,
|
|
},
|
|
{
|
|
"name": "twinkle",
|
|
"pattern": "twinkle",
|
|
"colors": ["#78C8FF", "#508CFF", "#B478FF", "#64DCE8", "#A0C8FF"],
|
|
"brightness": 255,
|
|
"delay": 55,
|
|
"auto": True,
|
|
"n1": 72,
|
|
"n2": 140,
|
|
"n3": 2,
|
|
"n4": 6,
|
|
},
|
|
]
|
|
|
|
for preset_data in default_preset_defs:
|
|
pid = presets.create(profile_id)
|
|
presets.update(pid, preset_data)
|
|
default_preset_ids.append(str(pid))
|
|
|
|
default_tab_id = zones.create(name="default", names=["1"], presets=[default_preset_ids])
|
|
zones.update(default_tab_id, {
|
|
"presets_flat": default_preset_ids,
|
|
"default_preset": default_preset_ids[0] if default_preset_ids else None,
|
|
})
|
|
|
|
profile = profiles.read(profile_id) or {}
|
|
profile_tabs = profile.get("zones", []) if isinstance(profile.get("zones", []), list) else []
|
|
profile_tabs.append(str(default_tab_id))
|
|
|
|
if seed_dj_zone:
|
|
# Seed a DJ-focused zone with three starter presets.
|
|
seeded_preset_ids = []
|
|
preset_defs = [
|
|
{
|
|
"name": "DJ Rainbow",
|
|
"pattern": "rainbow",
|
|
"colors": [],
|
|
"brightness": 220,
|
|
"delay": 60,
|
|
"n1": 12,
|
|
},
|
|
{
|
|
"name": "DJ Single Color",
|
|
"pattern": "on",
|
|
"colors": ["#ff00ff"],
|
|
"brightness": 220,
|
|
"delay": 100,
|
|
},
|
|
{
|
|
"name": "DJ Transition",
|
|
"pattern": "transition",
|
|
"colors": ["#ff0000", "#00ff00", "#0000ff"],
|
|
"brightness": 220,
|
|
"delay": 250,
|
|
},
|
|
]
|
|
|
|
for preset_data in preset_defs:
|
|
pid = presets.create(profile_id)
|
|
presets.update(pid, preset_data)
|
|
seeded_preset_ids.append(str(pid))
|
|
|
|
dj_tab_id = zones.create(name="dj", names=["dj"], presets=[seeded_preset_ids])
|
|
zones.update(dj_tab_id, {
|
|
"presets_flat": seeded_preset_ids,
|
|
"default_preset": seeded_preset_ids[0] if seeded_preset_ids else None,
|
|
})
|
|
|
|
profile_tabs.append(str(dj_tab_id))
|
|
|
|
profiles.update(profile_id, {"zones": profile_tabs})
|
|
|
|
profile_data = profiles.read(profile_id)
|
|
return json.dumps({profile_id: profile_data}), 201, {'Content-Type': 'application/json'}
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)}), 400
|
|
|
|
@controller.post('/<id>/clone')
|
|
async def clone_profile(request, id):
|
|
"""Clone an existing profile along with its tabs and palette."""
|
|
try:
|
|
source = profiles.read(id)
|
|
if not source:
|
|
return json.dumps({"error": "Profile not found"}), 404
|
|
|
|
data = request.json or {}
|
|
source_name = source.get("name") or f"Profile {id}"
|
|
new_name = data.get("name") or source_name
|
|
profile_type = source.get("type", "zones")
|
|
|
|
def allocate_id(model, cache):
|
|
if "next" not in cache:
|
|
max_id = max((int(k) for k in model.keys() if str(k).isdigit()), default=0)
|
|
cache["next"] = max_id + 1
|
|
next_id = str(cache["next"])
|
|
cache["next"] += 1
|
|
return next_id
|
|
|
|
def map_preset_container(value, id_map, preset_cache, new_profile_id, new_presets):
|
|
if isinstance(value, list):
|
|
return [map_preset_container(v, id_map, preset_cache, new_profile_id, new_presets) for v in value]
|
|
if value is None:
|
|
return None
|
|
preset_id = str(value)
|
|
if preset_id in id_map:
|
|
return id_map[preset_id]
|
|
preset_data = presets.read(preset_id)
|
|
if not preset_data:
|
|
return None
|
|
new_preset_id = allocate_id(presets, preset_cache)
|
|
clone_data = dict(preset_data)
|
|
clone_data["profile_id"] = str(new_profile_id)
|
|
new_presets[new_preset_id] = clone_data
|
|
id_map[preset_id] = new_preset_id
|
|
return new_preset_id
|
|
|
|
# Prepare new IDs without writing until everything is ready.
|
|
profile_cache = {}
|
|
palette_cache = {}
|
|
tab_cache = {}
|
|
preset_cache = {}
|
|
|
|
new_profile_id = allocate_id(profiles, profile_cache)
|
|
new_palette_id = allocate_id(profiles._palette_model, palette_cache)
|
|
|
|
# Clone palette colors into the new profile's palette
|
|
src_palette_id = source.get("palette_id")
|
|
palette_colors = []
|
|
if src_palette_id:
|
|
try:
|
|
palette_colors = profiles._palette_model.read(src_palette_id)
|
|
except Exception:
|
|
palette_colors = []
|
|
|
|
# Clone tabs and presets used by those tabs
|
|
source_tabs = source.get("zones")
|
|
if not isinstance(source_tabs, list) or len(source_tabs) == 0:
|
|
source_tabs = source.get("zone_order", [])
|
|
source_tabs = source_tabs or []
|
|
cloned_tab_ids = []
|
|
preset_id_map = {}
|
|
new_tabs = {}
|
|
new_presets = {}
|
|
for zone_id in source_tabs:
|
|
zone = zones.read(zone_id)
|
|
if not zone:
|
|
continue
|
|
tab_name = zone.get("name") or f"Zone {zone_id}"
|
|
clone_name = tab_name
|
|
mapped_presets = map_preset_container(zone.get("presets"), preset_id_map, preset_cache, new_profile_id, new_presets)
|
|
clone_id = allocate_id(zones, tab_cache)
|
|
clone_data = {
|
|
"name": clone_name,
|
|
"names": zone.get("names") or [],
|
|
"presets": mapped_presets if mapped_presets is not None else []
|
|
}
|
|
extra = {k: v for k, v in zone.items() if k not in ("name", "names", "presets")}
|
|
if "presets_flat" in extra:
|
|
extra["presets_flat"] = map_preset_container(extra.get("presets_flat"), preset_id_map, preset_cache, new_profile_id, new_presets)
|
|
if extra:
|
|
clone_data.update(extra)
|
|
new_tabs[clone_id] = clone_data
|
|
cloned_tab_ids.append(clone_id)
|
|
|
|
new_profile_data = {
|
|
"name": new_name,
|
|
"type": profile_type,
|
|
"zones": cloned_tab_ids,
|
|
"scenes": list(source.get("scenes", [])) if isinstance(source.get("scenes", []), list) else [],
|
|
"palette_id": str(new_palette_id),
|
|
}
|
|
|
|
# Commit all changes and save once per model.
|
|
profiles._palette_model[str(new_palette_id)] = list(palette_colors) if palette_colors else []
|
|
for pid, pdata in new_presets.items():
|
|
presets[pid] = pdata
|
|
for tid, tdata in new_tabs.items():
|
|
zones[tid] = tdata
|
|
profiles[str(new_profile_id)] = new_profile_data
|
|
|
|
profiles._palette_model.save()
|
|
presets.save()
|
|
zones.save()
|
|
profiles.save()
|
|
|
|
return json.dumps({new_profile_id: new_profile_data}), 201, {'Content-Type': 'application/json'}
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)}), 400
|
|
|
|
@controller.put('/current')
|
|
@with_session
|
|
async def update_current_profile(request, session):
|
|
"""Update the current profile using session (or fallback)."""
|
|
try:
|
|
data = request.json or {}
|
|
profile_list = profiles.list()
|
|
current_id = session.get('current_profile')
|
|
if not current_id and profile_list:
|
|
current_id = profile_list[0]
|
|
session['current_profile'] = str(current_id)
|
|
session.save()
|
|
if not current_id:
|
|
return json.dumps({"error": "No profile available"}), 404
|
|
if profiles.update(current_id, data):
|
|
return json.dumps(profiles.read(current_id)), 200, {'Content-Type': 'application/json'}
|
|
return json.dumps({"error": "Profile not found"}), 404
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)}), 400
|
|
|
|
@controller.put('/<id>')
|
|
async def update_profile(request, id):
|
|
"""Update an existing profile."""
|
|
try:
|
|
data = request.json
|
|
if profiles.update(id, data):
|
|
return json.dumps(profiles.read(id)), 200, {'Content-Type': 'application/json'}
|
|
return json.dumps({"error": "Profile not found"}), 404
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)}), 400
|
|
|
|
@controller.delete('/<id>')
|
|
async def delete_profile(request, id):
|
|
"""Delete a profile."""
|
|
if profiles.delete(id):
|
|
return json.dumps({"message": "Profile deleted successfully"}), 200
|
|
return json.dumps({"error": "Profile not found"}), 404
|