stagedings 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. stagedings-0.1.0/LICENSE.md +7 -0
  2. stagedings-0.1.0/PKG-INFO +150 -0
  3. stagedings-0.1.0/README.md +125 -0
  4. stagedings-0.1.0/pyproject.toml +59 -0
  5. stagedings-0.1.0/setup.cfg +4 -0
  6. stagedings-0.1.0/src/stagedings/__init__.py +0 -0
  7. stagedings-0.1.0/src/stagedings/cli.py +296 -0
  8. stagedings-0.1.0/src/stagedings/core/__init__.py +0 -0
  9. stagedings-0.1.0/src/stagedings/core/connection.py +33 -0
  10. stagedings-0.1.0/src/stagedings/core/control.py +59 -0
  11. stagedings-0.1.0/src/stagedings/core/osc.py +53 -0
  12. stagedings-0.1.0/src/stagedings/core/scene.py +59 -0
  13. stagedings-0.1.0/src/stagedings/models/__init__.py +0 -0
  14. stagedings-0.1.0/src/stagedings/models/base_model.py +14 -0
  15. stagedings-0.1.0/src/stagedings/static/config.json +7 -0
  16. stagedings-0.1.0/src/stagedings/static/css/cyborg/bootstrap.min.css +12 -0
  17. stagedings-0.1.0/src/stagedings/static/js/bootstrap.min.js +7 -0
  18. stagedings-0.1.0/src/stagedings/static/js/bootstrap.min.js.map +1 -0
  19. stagedings-0.1.0/src/stagedings/static/js/jquery-4.0.0.slim.min.js +2 -0
  20. stagedings-0.1.0/src/stagedings/templates/base.html +23 -0
  21. stagedings-0.1.0/src/stagedings/templates/nav.html +25 -0
  22. stagedings-0.1.0/src/stagedings/templates/no_context.html +18 -0
  23. stagedings-0.1.0/src/stagedings/templates/ui.html +310 -0
  24. stagedings-0.1.0/src/stagedings.egg-info/PKG-INFO +150 -0
  25. stagedings-0.1.0/src/stagedings.egg-info/SOURCES.txt +27 -0
  26. stagedings-0.1.0/src/stagedings.egg-info/dependency_links.txt +1 -0
  27. stagedings-0.1.0/src/stagedings.egg-info/entry_points.txt +2 -0
  28. stagedings-0.1.0/src/stagedings.egg-info/requires.txt +6 -0
  29. stagedings-0.1.0/src/stagedings.egg-info/top_level.txt +1 -0
@@ -0,0 +1,7 @@
1
+ # License
2
+
3
+ stagedings is available under the terms of the
4
+ [GNU General Public License, version 2 or later][spdx-gpl2].
5
+
6
+
7
+ [spdx-gpl2]: https://spdx.org/licenses/GPL-2.0-or-later.html
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: stagedings
3
+ Version: 0.1.0
4
+ Summary: An API to navigate scenes and subscenes that has been configured in a mididings script
5
+ Author-email: Stéphane Gagnon <stephane-gagnon@hotmail.com>
6
+ Maintainer-email: Stéphane Gagnon <stephane-gagnon@hotmail.com>
7
+ License-Expression: GPL-2.0-or-later
8
+ Project-URL: Homepage, https://github.com/mididings
9
+ Project-URL: Documentation, https://mididings.github.io/stagedings/
10
+ Project-URL: Repository, https://github.com/mididings/stagedings
11
+ Keywords: mididings,midi,live-performance,music,control-surface,fastapi,websocket,osc
12
+ Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE.md
18
+ Requires-Dist: fastapi
19
+ Requires-Dist: uvicorn[standard]
20
+ Requires-Dist: jinja2
21
+ Requires-Dist: pydantic
22
+ Requires-Dist: scalar-fastapi
23
+ Requires-Dist: mididings[autorestart,osc]
24
+ Dynamic: license-file
25
+
26
+ # stagedings
27
+ An API to navigate scenes and subscenes that has been configured in a [mididings](https://github.com/mididings/mididings) script
28
+
29
+ [![PyPI](https://img.shields.io/pypi/v/stagedings)](https://pypi.org/project/stagedings/)
30
+ ![Python](https://img.shields.io/badge/python-3.9%2B-blue)
31
+ [![OpenAPI Spec](https://img.shields.io/badge/OpenAPI-Yes-green)](https://swagger.io/specification/)
32
+ [![Discourse](https://img.shields.io/badge/community-Discourse-orange)](https://mididings.discourse.group/)
33
+
34
+
35
+ ---
36
+ ### What does stagedings allow?
37
+ * A web-based interface
38
+ * Alternative of the legacy **`livedings UI`**, which was based on Tkinter 🪓
39
+ * A HTTP layer that facilitates control and navigation allowing the abstraction of OSC subcalls
40
+ * An OpenAPI specification making possible to generate a client SDK in multiple language with a code generator like [Kiota](https://github.com/microsoft/kiota) making possible to use the API in .NET, Go, Java, PHP, Python, Ruby and TypeScript.
41
+
42
+ ⚠️ *A scene patch dictionary defined in the `run` section of your mididings script is required to work correctly, check the [`run` function documentation for more information](https://mididings.github.io/mididings/main.html#mididings.run) on how to structure your patch.*
43
+
44
+
45
+ ## 📘 API documentation
46
+ - [stagedings manual](https://mididings.github.io/stagedings)
47
+ - [mididings manual](https://mididings.github.io/mididings)
48
+
49
+ ## Frontend
50
+
51
+ ### A responsive multiclient, real-time interface for scene/subscene navigation
52
+
53
+ <img src="https://raw.githubusercontent.com/mididings/stagedings/main/docs/frontend.png" alt="stagedings UI screenshot" width="700"/>
54
+
55
+ ---
56
+ ## Features
57
+ - Web UI with real-time scene/subscene updates
58
+ - FastAPI backend with full REST and WebSocket support
59
+ - Multiple clients supported
60
+ - Use the mididings OSC interface
61
+ - It exposes a **fully compliant OpenAPI spec** for easy generation of SDK clients in any language, enabling flexible remote control of mididings
62
+
63
+ ---
64
+
65
+ ### The frontend allow
66
+ * Direct navigation through scenes and subscenes
67
+ * Exposes the Restart, Panic, Query and Quit commands
68
+
69
+ ### The backend allow
70
+ * Endpoints for direct navigation through scenes and subscenes
71
+ * Endpoints to the Restart, Panic, Query and Quit commands
72
+
73
+ #### ℹ️ About commands
74
+ * ***Restart*** will restart mididings process
75
+ * ***Panic*** send not off to all ports and all channels
76
+ * ***Quit*** stop mididings, be carefull
77
+ * *Query is a work in progress*
78
+
79
+
80
+ ---
81
+
82
+ ## ⚒️ Installation from PyPI
83
+ **NOTE:** This will also install mididings with OSC and AutoRestart support allowing a full working stack.
84
+
85
+ ```sh
86
+ # Create a Python virtual environment
87
+ $ python3 -m venv .venv
88
+ $ source .venv/bin/activate
89
+
90
+ # Install stagedings including mididings as a dependency
91
+ $ pip install stagedings
92
+ ```
93
+ ## ▶️ Running the application
94
+ ```sh
95
+ $ stagedings [--host HOST] [--port PORT]
96
+ ```
97
+ ## Options
98
+ * --host
99
+ * FastAPI server bind address
100
+ * Default: localhost
101
+ * Use 0.0.0.0 to allow network access or the server IP address
102
+ * --port
103
+ * FastAPI + WebSocket server port
104
+ * Default: 5000
105
+
106
+ ## Use cases
107
+ ### Local development (single machine)
108
+ ```sh
109
+ $ stagedings
110
+ ```
111
+ This runs everything locally on:
112
+ ```sh
113
+ http://localhost:5000
114
+ ```
115
+ ### Network / multi-client setup
116
+ When clients access the server from other machines, you must expose the backend:
117
+ ```sh
118
+ $ stagedings --host 0.0.0.0
119
+ ```
120
+ or:
121
+ ```sh
122
+ $ stagedings --host 192.168.1.100
123
+ ```
124
+ ### Accessing the UI
125
+ Once running, open in a browser:
126
+ ```sh
127
+ http://dings.local.com:5000
128
+ ```
129
+ or:
130
+ ```sh
131
+ http://192.168.1.100:5000
132
+ ```
133
+
134
+ ## High Level Overview
135
+ <img src="https://raw.githubusercontent.com/mididings/stagedings/main/docs/overview.png" alt="stagedings UI screenshot" width="800"/>
136
+
137
+ ## 🔗 Communication Workflow
138
+ <img src="https://raw.githubusercontent.com/mididings/stagedings/main/docs/workflow.png" alt="stagedings UI screenshot" width="800"/>
139
+
140
+ ### 💬 Feedback & Contributions
141
+
142
+ We welcome bug reports, feature ideas, and contributions! Please open an issue or discussion
143
+
144
+ ### 📜 License
145
+
146
+ All files in this repository are released under the terms of the GNU
147
+ General Public License as published by the Free Software Foundation;
148
+ either version 2 or later of the License.
149
+
150
+ Made in Québec 🇨🇦
@@ -0,0 +1,125 @@
1
+ # stagedings
2
+ An API to navigate scenes and subscenes that has been configured in a [mididings](https://github.com/mididings/mididings) script
3
+
4
+ [![PyPI](https://img.shields.io/pypi/v/stagedings)](https://pypi.org/project/stagedings/)
5
+ ![Python](https://img.shields.io/badge/python-3.9%2B-blue)
6
+ [![OpenAPI Spec](https://img.shields.io/badge/OpenAPI-Yes-green)](https://swagger.io/specification/)
7
+ [![Discourse](https://img.shields.io/badge/community-Discourse-orange)](https://mididings.discourse.group/)
8
+
9
+
10
+ ---
11
+ ### What does stagedings allow?
12
+ * A web-based interface
13
+ * Alternative of the legacy **`livedings UI`**, which was based on Tkinter 🪓
14
+ * A HTTP layer that facilitates control and navigation allowing the abstraction of OSC subcalls
15
+ * An OpenAPI specification making possible to generate a client SDK in multiple language with a code generator like [Kiota](https://github.com/microsoft/kiota) making possible to use the API in .NET, Go, Java, PHP, Python, Ruby and TypeScript.
16
+
17
+ ⚠️ *A scene patch dictionary defined in the `run` section of your mididings script is required to work correctly, check the [`run` function documentation for more information](https://mididings.github.io/mididings/main.html#mididings.run) on how to structure your patch.*
18
+
19
+
20
+ ## 📘 API documentation
21
+ - [stagedings manual](https://mididings.github.io/stagedings)
22
+ - [mididings manual](https://mididings.github.io/mididings)
23
+
24
+ ## Frontend
25
+
26
+ ### A responsive multiclient, real-time interface for scene/subscene navigation
27
+
28
+ <img src="https://raw.githubusercontent.com/mididings/stagedings/main/docs/frontend.png" alt="stagedings UI screenshot" width="700"/>
29
+
30
+ ---
31
+ ## Features
32
+ - Web UI with real-time scene/subscene updates
33
+ - FastAPI backend with full REST and WebSocket support
34
+ - Multiple clients supported
35
+ - Use the mididings OSC interface
36
+ - It exposes a **fully compliant OpenAPI spec** for easy generation of SDK clients in any language, enabling flexible remote control of mididings
37
+
38
+ ---
39
+
40
+ ### The frontend allow
41
+ * Direct navigation through scenes and subscenes
42
+ * Exposes the Restart, Panic, Query and Quit commands
43
+
44
+ ### The backend allow
45
+ * Endpoints for direct navigation through scenes and subscenes
46
+ * Endpoints to the Restart, Panic, Query and Quit commands
47
+
48
+ #### ℹ️ About commands
49
+ * ***Restart*** will restart mididings process
50
+ * ***Panic*** send not off to all ports and all channels
51
+ * ***Quit*** stop mididings, be carefull
52
+ * *Query is a work in progress*
53
+
54
+
55
+ ---
56
+
57
+ ## ⚒️ Installation from PyPI
58
+ **NOTE:** This will also install mididings with OSC and AutoRestart support allowing a full working stack.
59
+
60
+ ```sh
61
+ # Create a Python virtual environment
62
+ $ python3 -m venv .venv
63
+ $ source .venv/bin/activate
64
+
65
+ # Install stagedings including mididings as a dependency
66
+ $ pip install stagedings
67
+ ```
68
+ ## ▶️ Running the application
69
+ ```sh
70
+ $ stagedings [--host HOST] [--port PORT]
71
+ ```
72
+ ## Options
73
+ * --host
74
+ * FastAPI server bind address
75
+ * Default: localhost
76
+ * Use 0.0.0.0 to allow network access or the server IP address
77
+ * --port
78
+ * FastAPI + WebSocket server port
79
+ * Default: 5000
80
+
81
+ ## Use cases
82
+ ### Local development (single machine)
83
+ ```sh
84
+ $ stagedings
85
+ ```
86
+ This runs everything locally on:
87
+ ```sh
88
+ http://localhost:5000
89
+ ```
90
+ ### Network / multi-client setup
91
+ When clients access the server from other machines, you must expose the backend:
92
+ ```sh
93
+ $ stagedings --host 0.0.0.0
94
+ ```
95
+ or:
96
+ ```sh
97
+ $ stagedings --host 192.168.1.100
98
+ ```
99
+ ### Accessing the UI
100
+ Once running, open in a browser:
101
+ ```sh
102
+ http://dings.local.com:5000
103
+ ```
104
+ or:
105
+ ```sh
106
+ http://192.168.1.100:5000
107
+ ```
108
+
109
+ ## High Level Overview
110
+ <img src="https://raw.githubusercontent.com/mididings/stagedings/main/docs/overview.png" alt="stagedings UI screenshot" width="800"/>
111
+
112
+ ## 🔗 Communication Workflow
113
+ <img src="https://raw.githubusercontent.com/mididings/stagedings/main/docs/workflow.png" alt="stagedings UI screenshot" width="800"/>
114
+
115
+ ### 💬 Feedback & Contributions
116
+
117
+ We welcome bug reports, feature ideas, and contributions! Please open an issue or discussion
118
+
119
+ ### 📜 License
120
+
121
+ All files in this repository are released under the terms of the GNU
122
+ General Public License as published by the Free Software Foundation;
123
+ either version 2 or later of the License.
124
+
125
+ Made in Québec 🇨🇦
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["setuptools>=80"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "stagedings"
7
+ version = "0.1.0"
8
+ description = "An API to navigate scenes and subscenes that has been configured in a mididings script"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "GPL-2.0-or-later"
12
+ license-files = ["LICENSE.md"]
13
+ keywords = [
14
+ "mididings",
15
+ "midi",
16
+ "live-performance",
17
+ "music",
18
+ "control-surface",
19
+ "fastapi",
20
+ "websocket",
21
+ "osc"
22
+ ]
23
+
24
+ authors = [
25
+ { name = "Stéphane Gagnon", email = "stephane-gagnon@hotmail.com" }
26
+ ]
27
+
28
+ maintainers = [
29
+ { name = "Stéphane Gagnon", email = "stephane-gagnon@hotmail.com" }
30
+ ]
31
+
32
+ dependencies = [
33
+ "fastapi",
34
+ "uvicorn[standard]",
35
+ "jinja2",
36
+ "pydantic",
37
+ "scalar-fastapi",
38
+ "mididings[osc,autorestart]",
39
+ ]
40
+
41
+ classifiers = [
42
+ "Topic :: Multimedia :: Sound/Audio :: MIDI",
43
+ "Programming Language :: Python :: 3",
44
+ "Development Status :: 5 - Production/Stable",
45
+ ]
46
+
47
+ [tool.setuptools.package-data]
48
+ stagedings = [
49
+ "templates/**/*",
50
+ "static/**/*"
51
+ ]
52
+
53
+ [project.scripts]
54
+ stagedings = "stagedings.cli:main"
55
+
56
+ [project.urls]
57
+ Homepage = "https://github.com/mididings"
58
+ Documentation = "https://mididings.github.io/stagedings/"
59
+ Repository = "https://github.com/mididings/stagedings"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # SPDX-License-Identifier: GPL-2.0-or-later
5
+
6
+ import argparse
7
+ import os
8
+ import json
9
+ import asyncio
10
+ import uvicorn
11
+
12
+ from pathlib import Path
13
+ from importlib import resources
14
+ import stagedings
15
+
16
+ from stagedings.core.control import Controller
17
+ from stagedings.core.connection import ConnectionManager
18
+
19
+ from fastapi import Request, FastAPI, WebSocket, WebSocketDisconnect, Response
20
+ from fastapi.responses import HTMLResponse
21
+ from fastapi.staticfiles import StaticFiles
22
+ from fastapi.templating import Jinja2Templates
23
+ from fastapi.openapi.utils import get_openapi
24
+ from scalar_fastapi import get_scalar_api_reference
25
+
26
+ BASE_DIR = Path(stagedings.__file__).resolve().parent
27
+
28
+ description = """
29
+ ### You will be able to:
30
+
31
+ * **Navigating Scenes and Subscenes**
32
+ * **Control mididings**
33
+ """
34
+
35
+ app = FastAPI()
36
+
37
+ def custom_openapi():
38
+ if app.openapi_schema:
39
+ return app.openapi_schema
40
+ app.openapi_schema = get_openapi(
41
+ title="stagedings",
42
+ version="1.0.0",
43
+ description=description,
44
+ routes=app.routes,
45
+ openapi_version="3.1.0",
46
+ summary="An UI and API for mididings community version"
47
+ )
48
+ app.openapi_schema["info"]["x-logo"] = {
49
+ "url": "https://avatars.githubusercontent.com/u/121540801?s=400&u=2d3daf12927631aecd807b2d6dfb90652cc22ae8&v=4"
50
+ }
51
+ return app.openapi_schema
52
+
53
+ app.openapi = custom_openapi
54
+
55
+
56
+ """ Configuration """
57
+
58
+ app.mount(
59
+ "/static",
60
+ StaticFiles(directory=str(BASE_DIR / "static")),
61
+ name="static"
62
+ )
63
+ templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
64
+
65
+ config = resources.files(stagedings) / "static" / "config.json"
66
+ with open(config) as FILE:
67
+ configuration = json.load(FILE)
68
+
69
+
70
+ # Mididings and OSC context
71
+ controller = Controller(configuration["osc_server"])
72
+
73
+ # WebSocket connection manager
74
+ connection_manager = ConnectionManager()
75
+
76
+ async def mididings_context_update():
77
+ await controller.set_dirty(False)
78
+ await connection_manager.broadcast(
79
+ {"action": "mididings_context_update", "payload": controller.scene_controller.payload}
80
+ )
81
+
82
+ # UI enpoints
83
+ @app.get("/scalar", include_in_schema=False)
84
+ async def scalar_html():
85
+ return get_scalar_api_reference(
86
+ # Your OpenAPI document
87
+ openapi_url=app.openapi_url,
88
+ # Avoid CORS issues (optional)
89
+ # scalar_proxy_url="https://proxy.scalar.com",
90
+ )
91
+
92
+
93
+ @app.get("/", response_class=HTMLResponse, include_in_schema=False)
94
+ async def entry_point(request: Request):
95
+ return templates.TemplateResponse(
96
+ name="ui.html" if controller.scene_controller.scenes else "no_context.html",
97
+ context={
98
+ "request": request
99
+ },
100
+ request=request,
101
+ )
102
+
103
+ # Navigation endpoints
104
+ # -----
105
+ @app.post("/api/scenes/{sceneId}/subscenes/{subsceneId}/activate",
106
+ description="Switch to the given scene and subscene number.",
107
+ summary="Switch to the given scene and subscene number.",
108
+ tags=["Navigation"], responses={204: {"description": "No content"}}
109
+ )
110
+ async def switch_scene(sceneId: int, subsceneId: int):
111
+ await controller.switch_scene(sceneId)
112
+ await controller.switch_subscene(subsceneId)
113
+ return Response(status_code=204)
114
+
115
+ # -----
116
+ @app.post("/api/scenes/{sceneId}/activate",
117
+ description="Switch to the given scene number.",
118
+ summary="Switch to the given scene number.",
119
+ tags=["Navigation"], responses={204: {"description": "No content"}}
120
+ )
121
+ async def switch_scene(sceneId: int):
122
+ await controller.switch_scene(sceneId)
123
+ return Response(status_code=204)
124
+
125
+ # -----
126
+ @app.post("/api/subscenes/{subsceneId}/activate",
127
+ description="Switch to the given subscene number.",
128
+ summary="Switch to the given subscene number.",
129
+ tags=["Navigation"], responses={204: {"description": "No content"}}
130
+ )
131
+ async def switch_subscene(subsceneId: int):
132
+ await controller.switch_subscene(subsceneId)
133
+ return Response(status_code=204)
134
+
135
+ # -----
136
+ @app.post("/api/scenes/prev", description="Switch to the previous scene.",
137
+ summary="Switch to the previous scene.", tags=["Navigation"], responses={204: {"description": "No content"}}
138
+ )
139
+ async def prev_scene():
140
+ await controller.prev_scene()
141
+ return Response(status_code=204)
142
+
143
+ # -----
144
+ @app.post("/api/scenes/next", description="Switch to the next scene.",
145
+ summary="Switch to the next scene.", tags=["Navigation"], responses={204: {"description": "No content"}}
146
+ )
147
+ async def next_scene():
148
+ await controller.next_scene()
149
+ return Response(status_code=204)
150
+
151
+ # -----
152
+ @app.post("/api/subscenes/prev", description="Switch to the previous subscene.",
153
+ summary="Switch to the previous subscene.",
154
+ tags=["Navigation"], responses={204: {"description": "No content"}}
155
+ )
156
+ async def prev_subscene():
157
+ await controller.prev_subscene()
158
+ return Response(status_code=204)
159
+
160
+ # -----
161
+ @app.post("/api/subscenes/next", description="Switch to the next subscene.",
162
+ summary="Switch to the next subscene.", tags=["Navigation"], responses={204: {"description": "No content"}}
163
+ )
164
+ async def next_subscene():
165
+ await controller.next_subscene()
166
+ return Response(status_code=204)
167
+
168
+ # System endpoints
169
+ # -----
170
+ @app.post("/api/system/panic", description="Send all-notes-off on all channels and on all output ports.",
171
+ summary="Send all-notes-off on all channels and on all output ports.",
172
+ tags=["System"], responses={204: {"description": "No content"}})
173
+ async def panic():
174
+ await controller.panic()
175
+ return Response(status_code=204)
176
+
177
+ # -----
178
+ @app.post("/api/system/quit", description="Terminate mididings.", summary="Terminate mididings.",
179
+ tags=["System"], responses={204: {"description": "No content"}}
180
+ )
181
+ async def quit():
182
+ await controller.quit()
183
+ return Response(status_code=204)
184
+
185
+ # -----
186
+ @app.post("/api/system/restart", description="Restart mididings.", summary="Restart mididings.",
187
+ tags=["System"], responses={204: {"description": "No content"}}
188
+ )
189
+ async def restart():
190
+ await controller.restart()
191
+ return Response(status_code=204)
192
+
193
+ # -----
194
+ @app.post("/api/system/query", description="Send config, current scene/subscene to all notify ports.",
195
+ summary="Send config, current scene/subscene to all notify ports.",
196
+ tags=["System"], responses={204: {"description": "No content"}}
197
+ )
198
+ async def query():
199
+ await controller.query()
200
+ return Response(status_code=204)
201
+
202
+ """ Websocket handler """
203
+
204
+
205
+ @app.websocket("/ws")
206
+ async def websocket_endpoint(websocket: WebSocket):
207
+ await connection_manager.connect(websocket)
208
+ try:
209
+ while websocket in connection_manager.active_connections:
210
+ # Send status periodic task
211
+ await connection_manager.broadcast(
212
+ {"action": "on_start" if await controller.is_running() else "on_exit"}
213
+ )
214
+
215
+ try:
216
+ # Handle incoming messages from the client
217
+ data = await asyncio.wait_for(websocket.receive_json(), timeout=0.1)
218
+ action = data["action"]
219
+ if action in delegates:
220
+ (
221
+ await delegates[action]()
222
+ if not "id" in data
223
+ else await delegates[action](int(data["id"]))
224
+ )
225
+ except asyncio.TimeoutError:
226
+ # No message received during the timeout, continue the loop
227
+ pass
228
+
229
+ if await controller.is_dirty():
230
+ await delegates["mididings_context_update"]()
231
+
232
+ except WebSocketDisconnect:
233
+ connection_manager.disconnect(websocket)
234
+ except asyncio.exceptions.CancelledError:
235
+ print("asyncio CancelledError exception")
236
+ except Exception as e:
237
+ print(f"Unexpected WebSocket error: {e}")
238
+ connection_manager.disconnect(websocket)
239
+ finally:
240
+ print("exit")
241
+
242
+
243
+ async def on_quit(websocket: WebSocket = None):
244
+ await connection_manager.broadcast(
245
+ {
246
+ "action": "on_terminate",
247
+ }
248
+ )
249
+
250
+
251
+ async def on_connect(websocket: WebSocket = None):
252
+ await controller.set_dirty(True)
253
+
254
+
255
+ delegates = {
256
+
257
+ "on_connect": on_connect,
258
+
259
+ "quit": controller.quit,
260
+ "panic": controller.panic,
261
+ "query": controller.query,
262
+ "restart": controller.restart,
263
+
264
+ "next_scene": controller.next_scene,
265
+ "prev_scene": controller.prev_scene,
266
+ "next_subscene": controller.next_subscene,
267
+ "prev_subscene": controller.prev_subscene,
268
+
269
+ "switch_scene": controller.switch_scene,
270
+ "switch_subscene": controller.switch_subscene,
271
+
272
+ "mididings_context_update": mididings_context_update,
273
+
274
+ }
275
+
276
+ def main():
277
+
278
+ parser = argparse.ArgumentParser(description="Run stagedings FastAPI server")
279
+ parser.add_argument(
280
+ "--host",
281
+ default="localhost",
282
+ help="FastAPI listen host",
283
+ )
284
+ parser.add_argument(
285
+ "--port",
286
+ type=int,
287
+ default=5000,
288
+ help="FastAPI listen port",
289
+ )
290
+ args = parser.parse_args()
291
+
292
+ uvicorn.run(
293
+ "stagedings.cli:app",
294
+ host=args.host,
295
+ port=args.port,
296
+ )
File without changes
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # SPDX-License-Identifier: GPL-2.0-or-later
5
+
6
+ from fastapi import WebSocket
7
+ from typing import List
8
+
9
+ class ConnectionManager:
10
+ def __init__(self):
11
+ self.active_connections: List[WebSocket] = []
12
+
13
+ async def connect(self, websocket: WebSocket):
14
+ await websocket.accept()
15
+ if websocket not in self.active_connections:
16
+ print(f"Connecting: {websocket.client}")
17
+ self.active_connections.append(websocket)
18
+
19
+ def disconnect(self, websocket: WebSocket):
20
+ if websocket in self.active_connections:
21
+ print(f"Disconnecting: {websocket.client}")
22
+ try:
23
+ self.active_connections.remove(websocket)
24
+ except:
25
+ pass
26
+
27
+ async def broadcast(self, message: dict):
28
+ for websocket in self.active_connections[:]:
29
+ try:
30
+ await websocket.send_json(message)
31
+ except Exception as e:
32
+ print(f"WebSocket error: {e} for {websocket.client}")
33
+ self.disconnect(websocket)