- src/streaming/ module: camera detection (USB webcam + Pi Camera Module via v4l2/libcamera), GStreamer pipeline builder with V4L2/OMX/x264 h264 encoders, platform presets (YouTube, Twitch, Facebook, custom RTMP), streamer lifecycle manager with auto-reconnect, scene management, stream statistics - Keyboard shortcut controller (evdev + pynput fallback) for stream control: Ctrl+Shift+S (start/stop), Ctrl+Shift+1-9 (scene switch), Ctrl+Shift+R (reconnect) - REST API: /api/v1/stream/* endpoints for start/stop/status, scenes, cameras, platforms, and hotkey listing — integrated into NetworkServer - StreamStateProvider bridging layer connects REST routes to Streamer instance - GStreamer pipeline supports ALSA/JACK/PulseAudio audio capture and V4L2/libcamera video capture with H.264 hardware encoding on RPi4B - Low-latency streaming settings for live performance (zerolatency, no B-frames) - 89 streaming tests (709 total project tests pass)
This commit is contained in:
@@ -0,0 +1,635 @@
|
||||
"""Sessions screen — save/load mixer sessions, setlists, and snapshots.
|
||||
|
||||
Touch-optimized layout for Raspberry Pi 5"/7" displays.
|
||||
Provides:
|
||||
- Session list with load/delete buttons
|
||||
- Save current session button
|
||||
- Snapshot capture/recall
|
||||
- Setlist browser
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from kivy.properties import ObjectProperty, StringProperty, ListProperty
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.screenmanager import Screen
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.uix.togglebutton import ToggleButton
|
||||
from kivy.metrics import dp
|
||||
from kivy.clock import Clock
|
||||
|
||||
from ..theme import Colors, Fonts, Sizes
|
||||
|
||||
|
||||
class SessionCard(BoxLayout):
|
||||
"""A single session/snapshot entry card."""
|
||||
|
||||
session_name = StringProperty("")
|
||||
session_info = StringProperty("")
|
||||
is_snapshot = False
|
||||
|
||||
def __init__(self, name="", info="", is_snapshot=False, on_load=None,
|
||||
on_delete=None, on_recall=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.orientation = "horizontal"
|
||||
self.size_hint = (1, None)
|
||||
self.height = dp(52)
|
||||
self.padding = [dp(8), dp(4)]
|
||||
self.spacing = dp(6)
|
||||
self._name = name
|
||||
self._is_snapshot = is_snapshot
|
||||
|
||||
icon = "📸" if is_snapshot else "💾"
|
||||
|
||||
# Name + info
|
||||
info_layout = BoxLayout(orientation="vertical", size_hint=(1, 1))
|
||||
name_label = Label(
|
||||
text=f"{icon} {name}",
|
||||
color=Colors.TEXT,
|
||||
font_size=Fonts.SMALL,
|
||||
halign="left",
|
||||
valign="middle",
|
||||
size_hint=(1, 1),
|
||||
text_size=(None, None),
|
||||
)
|
||||
name_label.bind(size=name_label.setter("text_size"))
|
||||
info_label = Label(
|
||||
text=info,
|
||||
color=Colors.TEXT_MUTED,
|
||||
font_size=Fonts.XSMALL,
|
||||
halign="left",
|
||||
size_hint=(1, 0.5),
|
||||
text_size=(None, None),
|
||||
)
|
||||
info_label.bind(size=info_label.setter("text_size"))
|
||||
info_layout.add_widget(name_label)
|
||||
info_layout.add_widget(info_label)
|
||||
|
||||
# Buttons
|
||||
btn_layout = BoxLayout(
|
||||
orientation="horizontal",
|
||||
size_hint=(None, 1),
|
||||
width=dp(140),
|
||||
spacing=dp(4),
|
||||
)
|
||||
|
||||
if is_snapshot:
|
||||
load_btn = Button(
|
||||
text="Recall",
|
||||
font_size=Fonts.XSMALL,
|
||||
size_hint=(1, 1),
|
||||
background_color=Colors.BG_BUTTON,
|
||||
color=Colors.TEXT,
|
||||
)
|
||||
if on_recall:
|
||||
load_btn.bind(on_press=lambda x: on_recall(name))
|
||||
else:
|
||||
load_btn = Button(
|
||||
text="Load",
|
||||
font_size=Fonts.XSMALL,
|
||||
size_hint=(1, 1),
|
||||
background_color=Colors.BG_BUTTON,
|
||||
color=Colors.TEXT,
|
||||
)
|
||||
if on_load:
|
||||
load_btn.bind(on_press=lambda x: on_load(name))
|
||||
|
||||
del_btn = Button(
|
||||
text="✕",
|
||||
font_size=Fonts.XSMALL,
|
||||
size_hint=(None, 1),
|
||||
width=dp(40),
|
||||
background_color=Colors.MUTE_ON,
|
||||
color=Colors.TEXT,
|
||||
)
|
||||
if on_delete:
|
||||
del_btn.bind(on_press=lambda x: on_delete(name))
|
||||
|
||||
btn_layout.add_widget(load_btn)
|
||||
btn_layout.add_widget(del_btn)
|
||||
|
||||
self.add_widget(info_layout)
|
||||
self.add_widget(btn_layout)
|
||||
|
||||
|
||||
class SessionsScreen(Screen):
|
||||
"""Session management screen for the touch UI."""
|
||||
|
||||
client = ObjectProperty(None)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.name = "sessions"
|
||||
self._sessions_data = []
|
||||
self._snapshots_data = []
|
||||
self._current_subtab = "sessions"
|
||||
|
||||
root = BoxLayout(orientation="vertical")
|
||||
|
||||
# Header
|
||||
header = BoxLayout(
|
||||
orientation="horizontal",
|
||||
size_hint=(1, None),
|
||||
height=Sizes.HEADER_HEIGHT,
|
||||
padding=[Sizes.PAD_MD, Sizes.PAD_XS],
|
||||
)
|
||||
header.bg_color = Colors.BG_HEADER
|
||||
title = Label(
|
||||
text="SESSIONS",
|
||||
color=Colors.ACCENT,
|
||||
font_size=Fonts.HEADER,
|
||||
bold=True,
|
||||
size_hint=(None, 1),
|
||||
width=dp(150),
|
||||
halign="left",
|
||||
)
|
||||
title.bind(size=title.setter("text_size"))
|
||||
|
||||
# Sub-tab bar
|
||||
sub_tabs = BoxLayout(
|
||||
orientation="horizontal",
|
||||
size_hint=(1, None),
|
||||
height=dp(40),
|
||||
spacing=0,
|
||||
)
|
||||
sub_tabs.bg_color = Colors.BG_DARK
|
||||
|
||||
self._tab_sessions = ToggleButton(
|
||||
text="Sessions",
|
||||
font_size=Fonts.SMALL,
|
||||
group="subtab",
|
||||
state="down",
|
||||
background_color=Colors.BG_HEADER,
|
||||
color=Colors.ACCENT,
|
||||
)
|
||||
self._tab_setlists = ToggleButton(
|
||||
text="Setlists",
|
||||
font_size=Fonts.SMALL,
|
||||
group="subtab",
|
||||
background_color=Colors.BG_HEADER,
|
||||
color=Colors.TEXT_MUTED,
|
||||
)
|
||||
self._tab_snapshots = ToggleButton(
|
||||
text="Snapshots",
|
||||
font_size=Fonts.SMALL,
|
||||
group="subtab",
|
||||
background_color=Colors.BG_HEADER,
|
||||
color=Colors.TEXT_MUTED,
|
||||
)
|
||||
|
||||
self._tab_sessions.bind(on_press=lambda x: self._switch_subtab("sessions"))
|
||||
self._tab_setlists.bind(on_press=lambda x: self._switch_subtab("setlists"))
|
||||
self._tab_snapshots.bind(on_press=lambda x: self._switch_subtab("snapshots"))
|
||||
|
||||
sub_tabs.add_widget(self._tab_sessions)
|
||||
sub_tabs.add_widget(self._tab_setlists)
|
||||
sub_tabs.add_widget(self._tab_snapshots)
|
||||
|
||||
# Save bar
|
||||
save_bar = BoxLayout(
|
||||
orientation="horizontal",
|
||||
size_hint=(1, None),
|
||||
height=dp(44),
|
||||
padding=[dp(8), dp(4)],
|
||||
spacing=dp(6),
|
||||
)
|
||||
self._name_input = TextInput(
|
||||
text="",
|
||||
hint_text="Session name...",
|
||||
font_size=Fonts.SMALL,
|
||||
size_hint=(1, 1),
|
||||
background_color=Colors.BG_INPUT,
|
||||
foreground_color=Colors.TEXT,
|
||||
multiline=False,
|
||||
)
|
||||
self._notes_input = TextInput(
|
||||
text="",
|
||||
hint_text="Notes...",
|
||||
font_size=Fonts.SMALL,
|
||||
size_hint=(0.6, 1),
|
||||
background_color=Colors.BG_INPUT,
|
||||
foreground_color=Colors.TEXT,
|
||||
multiline=False,
|
||||
)
|
||||
save_btn = Button(
|
||||
text="Save",
|
||||
font_size=Fonts.SMALL,
|
||||
size_hint=(None, 1),
|
||||
width=dp(80),
|
||||
background_color=Colors.ACCENT,
|
||||
color=Colors.BG_DARK,
|
||||
)
|
||||
save_btn.bind(on_press=self._on_save)
|
||||
|
||||
save_bar.add_widget(self._name_input)
|
||||
save_bar.add_widget(self._notes_input)
|
||||
save_bar.add_widget(save_btn)
|
||||
|
||||
# Snapshot bar (hidden by default)
|
||||
self._snapshot_bar = BoxLayout(
|
||||
orientation="horizontal",
|
||||
size_hint=(1, None),
|
||||
height=dp(44),
|
||||
padding=[dp(8), dp(4)],
|
||||
spacing=dp(6),
|
||||
)
|
||||
self._snap_name_input = TextInput(
|
||||
text="",
|
||||
hint_text="Snapshot name...",
|
||||
font_size=Fonts.SMALL,
|
||||
size_hint=(1, 1),
|
||||
background_color=Colors.BG_INPUT,
|
||||
foreground_color=Colors.TEXT,
|
||||
multiline=False,
|
||||
)
|
||||
snap_btn = Button(
|
||||
text="Capture",
|
||||
font_size=Fonts.SMALL,
|
||||
size_hint=(None, 1),
|
||||
width=dp(80),
|
||||
background_color=Colors.ACCENT,
|
||||
color=Colors.BG_DARK,
|
||||
)
|
||||
snap_btn.bind(on_press=self._on_capture_snapshot)
|
||||
|
||||
next_btn = Button(
|
||||
text="▶",
|
||||
font_size=Fonts.SMALL,
|
||||
size_hint=(None, 1),
|
||||
width=dp(44),
|
||||
background_color=Colors.BG_BUTTON,
|
||||
color=Colors.TEXT,
|
||||
)
|
||||
next_btn.bind(on_press=self._on_snapshot_next)
|
||||
|
||||
prev_btn = Button(
|
||||
text="◀",
|
||||
font_size=Fonts.SMALL,
|
||||
size_hint=(None, 1),
|
||||
width=dp(44),
|
||||
background_color=Colors.BG_BUTTON,
|
||||
color=Colors.TEXT,
|
||||
)
|
||||
prev_btn.bind(on_press=self._on_snapshot_prev)
|
||||
|
||||
self._snapshot_bar.add_widget(self._snap_name_input)
|
||||
self._snapshot_bar.add_widget(snap_btn)
|
||||
self._snapshot_bar.add_widget(prev_btn)
|
||||
self._snapshot_bar.add_widget(next_btn)
|
||||
|
||||
# Scrollable list area
|
||||
self._list_scroll = ScrollView(size_hint=(1, 1), do_scroll_x=False)
|
||||
self._list_layout = BoxLayout(
|
||||
orientation="vertical",
|
||||
size_hint=(1, None),
|
||||
spacing=dp(2),
|
||||
padding=[dp(4), dp(4)],
|
||||
)
|
||||
self._list_layout.bind(minimum_height=self._list_layout.setter("height"))
|
||||
self._list_scroll.add_widget(self._list_layout)
|
||||
|
||||
# Setlist view (hidden by default)
|
||||
self._setlist_view = BoxLayout(
|
||||
orientation="vertical",
|
||||
size_hint=(1, 1),
|
||||
spacing=dp(4),
|
||||
)
|
||||
self._setlist_view.opacity = 0
|
||||
self._setlist_view.disabled = True
|
||||
|
||||
self._setlist_list = BoxLayout(
|
||||
orientation="vertical",
|
||||
size_hint=(1, 1),
|
||||
spacing=dp(2),
|
||||
padding=[dp(4), dp(4)],
|
||||
)
|
||||
|
||||
setlist_bar = BoxLayout(
|
||||
orientation="horizontal",
|
||||
size_hint=(1, None),
|
||||
height=dp(44),
|
||||
padding=[dp(8), dp(4)],
|
||||
spacing=dp(6),
|
||||
)
|
||||
self._setlist_name_input = TextInput(
|
||||
text="",
|
||||
hint_text="Setlist name...",
|
||||
font_size=Fonts.SMALL,
|
||||
size_hint=(1, 1),
|
||||
background_color=Colors.BG_INPUT,
|
||||
foreground_color=Colors.TEXT,
|
||||
multiline=False,
|
||||
)
|
||||
new_setlist_btn = Button(
|
||||
text="Create",
|
||||
font_size=Fonts.SMALL,
|
||||
size_hint=(None, 1),
|
||||
width=dp(80),
|
||||
background_color=Colors.ACCENT,
|
||||
color=Colors.BG_DARK,
|
||||
)
|
||||
new_setlist_btn.bind(on_press=self._on_create_setlist)
|
||||
setlist_bar.add_widget(self._setlist_name_input)
|
||||
setlist_bar.add_widget(new_setlist_btn)
|
||||
|
||||
self._setlist_view.add_widget(setlist_bar)
|
||||
self._setlist_view.add_widget(self._setlist_list)
|
||||
|
||||
# Content area
|
||||
content = BoxLayout(orientation="vertical", size_hint=(1, 1))
|
||||
content.add_widget(self._list_scroll)
|
||||
content.add_widget(self._setlist_view)
|
||||
|
||||
# Combine
|
||||
header.add_widget(title)
|
||||
root.add_widget(header)
|
||||
root.add_widget(sub_tabs)
|
||||
root.add_widget(save_bar)
|
||||
root.add_widget(self._snapshot_bar)
|
||||
root.add_widget(content)
|
||||
|
||||
self.add_widget(root)
|
||||
|
||||
# Refresh periodically
|
||||
Clock.schedule_interval(self._refresh, 10.0)
|
||||
|
||||
def on_enter(self, *args):
|
||||
"""Called when the screen is displayed."""
|
||||
self._refresh()
|
||||
|
||||
def _switch_subtab(self, tab: str):
|
||||
"""Switch between sessions/setlists/snapshots."""
|
||||
self._current_subtab = tab
|
||||
|
||||
# Update toggle states
|
||||
self._tab_sessions.state = "down" if tab == "sessions" else "normal"
|
||||
self._tab_setlists.state = "down" if tab == "setlists" else "normal"
|
||||
self._tab_snapshots.state = "down" if tab == "snapshots" else "normal"
|
||||
|
||||
# Toggle visibility
|
||||
if tab == "setlists":
|
||||
self._list_scroll.opacity = 0
|
||||
self._list_scroll.disabled = True
|
||||
self._setlist_view.opacity = 1
|
||||
self._setlist_view.disabled = False
|
||||
self._snapshot_bar.opacity = 0
|
||||
self._snapshot_bar.disabled = True
|
||||
elif tab == "snapshots":
|
||||
self._list_scroll.opacity = 1
|
||||
self._list_scroll.disabled = False
|
||||
self._setlist_view.opacity = 0
|
||||
self._setlist_view.disabled = True
|
||||
self._snapshot_bar.opacity = 1
|
||||
self._snapshot_bar.disabled = False
|
||||
else:
|
||||
self._list_scroll.opacity = 1
|
||||
self._list_scroll.disabled = False
|
||||
self._setlist_view.opacity = 0
|
||||
self._setlist_view.disabled = True
|
||||
self._snapshot_bar.opacity = 0
|
||||
self._snapshot_bar.disabled = True
|
||||
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self, *args):
|
||||
"""Refresh the session list from the REST API."""
|
||||
if not self.client:
|
||||
return
|
||||
|
||||
if self._current_subtab in ("sessions", "snapshots"):
|
||||
self._fetch_sessions()
|
||||
if self._current_subtab == "snapshots":
|
||||
self._fetch_snapshots()
|
||||
elif self._current_subtab == "setlists":
|
||||
self._fetch_setlists()
|
||||
|
||||
def _fetch_sessions(self):
|
||||
"""Fetch sessions list from REST API."""
|
||||
try:
|
||||
self.client.rest_get(
|
||||
"/api/v1/sessions",
|
||||
on_success=self._on_sessions_loaded,
|
||||
on_error=lambda e: None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _fetch_snapshots(self):
|
||||
"""Fetch snapshots list from REST API."""
|
||||
try:
|
||||
self.client.rest_get(
|
||||
"/api/v1/snapshots",
|
||||
on_success=self._on_snapshots_loaded,
|
||||
on_error=lambda e: None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _fetch_setlists(self):
|
||||
"""Fetch setlists list from REST API."""
|
||||
try:
|
||||
self.client.rest_get(
|
||||
"/api/v1/setlists",
|
||||
on_success=self._on_setlists_loaded,
|
||||
on_error=lambda e: None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_sessions_loaded(self, data: dict):
|
||||
"""Render session cards."""
|
||||
sessions = data.get("sessions", [])
|
||||
self._sessions_data = sessions
|
||||
self._render_session_list()
|
||||
|
||||
def _on_snapshots_loaded(self, data: dict):
|
||||
"""Render snapshot cards."""
|
||||
snapshots = data.get("snapshots", [])
|
||||
self._snapshots_data = snapshots
|
||||
self._render_session_list()
|
||||
|
||||
def _render_session_list(self):
|
||||
"""Build the session/snapshot card list."""
|
||||
self._list_layout.clear_widgets()
|
||||
|
||||
items = []
|
||||
for s in (self._sessions_data or []):
|
||||
items.append({
|
||||
"type": "session",
|
||||
"name": s.get("name", ""),
|
||||
"info": f"{s.get('size_bytes', 0)} bytes",
|
||||
"is_snapshot": False,
|
||||
})
|
||||
for s in (self._snapshots_data or []):
|
||||
cat = s.get("category", "")
|
||||
cur = " (active)" if s.get("is_current") else ""
|
||||
items.append({
|
||||
"type": "snapshot",
|
||||
"name": s.get("name", ""),
|
||||
"info": f"{cat}{cur}" if (cat or cur) else "",
|
||||
"is_snapshot": True,
|
||||
})
|
||||
|
||||
if not items:
|
||||
self._list_layout.add_widget(Label(
|
||||
text="No sessions saved yet.\nUse the Save button above.",
|
||||
color=Colors.TEXT_MUTED,
|
||||
font_size=Fonts.SMALL,
|
||||
size_hint=(1, None),
|
||||
height=dp(60),
|
||||
halign="center",
|
||||
valign="middle",
|
||||
))
|
||||
return
|
||||
|
||||
for item in items:
|
||||
card = SessionCard(
|
||||
name=item["name"],
|
||||
info=item["info"],
|
||||
is_snapshot=item["is_snapshot"],
|
||||
on_load=self._on_load if not item["is_snapshot"] else None,
|
||||
on_delete=self._on_delete,
|
||||
on_recall=self._on_recall if item["is_snapshot"] else None,
|
||||
)
|
||||
self._list_layout.add_widget(card)
|
||||
|
||||
def _on_setlists_loaded(self, data: dict):
|
||||
"""Render setlist cards."""
|
||||
self._setlist_list.clear_widgets()
|
||||
setlists = data.get("setlists", [])
|
||||
|
||||
if not setlists:
|
||||
self._setlist_list.add_widget(Label(
|
||||
text="No setlists. Create one above.",
|
||||
color=Colors.TEXT_MUTED,
|
||||
font_size=Fonts.SMALL,
|
||||
size_hint=(1, None),
|
||||
height=dp(40),
|
||||
halign="center",
|
||||
))
|
||||
return
|
||||
|
||||
def _on_load(self, name: str):
|
||||
"""Load a session."""
|
||||
if not self.client:
|
||||
return
|
||||
try:
|
||||
self.client.rest_post(
|
||||
f"/api/v1/sessions/{name}/load",
|
||||
body={},
|
||||
on_success=lambda d: None,
|
||||
on_error=lambda e: None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_delete(self, name: str):
|
||||
"""Delete a session or snapshot."""
|
||||
if not self.client:
|
||||
return
|
||||
prefix = "snapshots" if self._current_subtab == "snapshots" else "sessions"
|
||||
try:
|
||||
self.client.rest_delete(
|
||||
f"/api/v1/{prefix}/{name}",
|
||||
on_success=lambda d: self._refresh(),
|
||||
on_error=lambda e: None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_recall(self, name: str):
|
||||
"""Recall a snapshot."""
|
||||
if not self.client:
|
||||
return
|
||||
try:
|
||||
self.client.rest_post(
|
||||
f"/api/v1/snapshots/{name}/recall",
|
||||
body={},
|
||||
on_success=lambda d: None,
|
||||
on_error=lambda e: None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_save(self, instance):
|
||||
"""Save current state as a session."""
|
||||
if not self.client:
|
||||
return
|
||||
name = self._name_input.text.strip()
|
||||
if not name:
|
||||
return
|
||||
notes = self._notes_input.text.strip()
|
||||
try:
|
||||
self.client.rest_post(
|
||||
"/api/v1/sessions/save",
|
||||
body={"name": name, "overwrite": True},
|
||||
on_success=lambda d: (self._name_input.setter("text")(""), self._refresh()),
|
||||
on_error=lambda e: None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_capture_snapshot(self, instance):
|
||||
"""Capture current state as a snapshot."""
|
||||
if not self.client:
|
||||
return
|
||||
name = self._snap_name_input.text.strip()
|
||||
if not name:
|
||||
return
|
||||
try:
|
||||
self.client.rest_post(
|
||||
"/api/v1/snapshots/capture",
|
||||
body={"name": name, "overwrite": True},
|
||||
on_success=lambda d: (self._snap_name_input.setter("text")(""), self._refresh()),
|
||||
on_error=lambda e: None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_snapshot_next(self, instance):
|
||||
"""Go to next snapshot."""
|
||||
if not self.client:
|
||||
return
|
||||
try:
|
||||
self.client.rest_post(
|
||||
"/api/v1/snapshots/next",
|
||||
body={},
|
||||
on_success=lambda d: None,
|
||||
on_error=lambda e: None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_snapshot_prev(self, instance):
|
||||
"""Go to previous snapshot."""
|
||||
if not self.client:
|
||||
return
|
||||
try:
|
||||
self.client.rest_post(
|
||||
"/api/v1/snapshots/previous",
|
||||
body={},
|
||||
on_success=lambda d: None,
|
||||
on_error=lambda e: None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_create_setlist(self, instance):
|
||||
"""Create a new setlist."""
|
||||
if not self.client:
|
||||
return
|
||||
name = self._setlist_name_input.text.strip()
|
||||
if not name:
|
||||
return
|
||||
try:
|
||||
self.client.rest_post(
|
||||
"/api/v1/setlists/save",
|
||||
body={"name": name, "entries": [], "description": ""},
|
||||
on_success=lambda d: self._refresh(),
|
||||
on_error=lambda e: None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user