Compare commits

..

3 commits

Author SHA1 Message Date
43ef5ddb81 Implemented simple admin info in server
All checks were successful
Check / mypy (push) Successful in 2m35s
Check / ruff (push) Successful in 17s
2025-06-11 01:15:50 +02:00
10bf362665 Update syng-web 2025-06-11 01:09:05 +02:00
29d6821db0 Added handler for queue to waiting room message in server 2025-05-25 10:39:18 +02:00
6 changed files with 128 additions and 11 deletions

View file

@ -111,6 +111,7 @@ def main() -> None:
server_parser.add_argument("--private", "-P", action="store_true", default=False)
server_parser.add_argument("--restricted", "-R", action="store_true", default=False)
server_parser.add_argument("--admin-password", "-A", default=None)
server_parser.add_argument("--admin-port", "-a", type=int, default=None)
args = parser.parse_args()

View file

@ -16,6 +16,7 @@ from __future__ import annotations
import asyncio
import datetime
import hashlib
import logging
import os
import random
import string
@ -194,6 +195,9 @@ class Server:
cors_allowed_origins="*", logger=True, engineio_logger=False, json=jsonencoder
)
self.app = web.Application()
self.runner = web.AppRunner(self.app)
self.admin_app = web.Application()
self.admin_runner = web.AppRunner(self.admin_app)
self.clients: dict[str, State] = {}
self.sio.attach(self.app)
self.register_handlers()
@ -208,6 +212,7 @@ class Server:
self.sio.on("meta-info", self.handle_meta_info)
self.sio.on("get-first", self.handle_get_first)
self.sio.on("waiting-room-to-queue", self.handle_waiting_room_to_queue)
self.sio.on("queue-to-waiting-room", self.handle_queue_to_waiting_room)
self.sio.on("pop-then-get-next", self.handle_pop_then_get_next)
self.sio.on("register-client", self.handle_register_client)
self.sio.on("sources", self.handle_sources)
@ -239,6 +244,68 @@ class Server:
return web.FileResponse(os.path.join(self.app["root_folder"], "favicon.ico"))
return web.FileResponse(os.path.join(self.app["root_folder"], "index.html"))
async def get_number_connections(self) -> int:
"""
Get the number of connections to the server.
:return: The number of connections
:rtype: int
"""
num = 0
for namespace in self.sio.manager.get_namespaces():
for room in self.sio.manager.rooms[namespace]:
for participant in self.sio.manager.get_participants(namespace, room):
num += 1
return num
async def get_clients(self, room: str) -> list[dict[str, Any]]:
"""
Get the number of clients in a room.
:param room: The room to get the number of clients for
:type room: str
:return: The number of clients in the room
:rtype: int
"""
clients = []
for sid, client_id in self.sio.manager.get_participants("/", room):
client: dict[str, Any] = {}
client["sid"] = sid
if sid == self.clients[room].sid:
client["type"] = "playback"
else:
client["type"] = "web"
client["admin"] = False
async with self.sio.session(sid) as session:
if "admin" in session:
client["admin"] = session["admin"]
clients.append(client)
return clients
async def admin_handler(self, request: Any) -> Any:
"""
Handle the admin request.
"""
rooms = [
{
"room": room,
"sid": state.sid,
"last_seen": state.last_seen.isoformat(),
"queue": state.queue.to_list(),
"waiting_room": state.waiting_room,
"clients": await self.get_clients(room),
}
for room, state in self.clients.items()
]
info_dict = {
"version": SYNG_VERSION,
"protocol_version": SYNG_PROTOCOL_VERSION,
"connections": await self.get_number_connections(),
"rooms": rooms,
}
return web.json_response(info_dict, dumps=jsonencoder.dumps)
async def broadcast_state(
self, state: State, /, sid: Optional[str] = None, room: Optional[str] = None
) -> None:
@ -1397,6 +1464,33 @@ class Server:
iapp["repeated_cleanup"].cancel()
await iapp["repeated_cleanup"]
async def run_apps(self, host: str, port: int, admin_port: Optional[int]) -> None:
"""
Run the main and admin apps.
This is used to run the main app and the admin app in parallel.
:param host: The host to bind to
:type host: str
:param port: The port to bind to
:type port: int
:param admin_port: The port for the admin interface, or None if not used
:type admin_port: Optional[int]
:rtype: None
"""
if admin_port:
logger.info("Starting admin interface on port %d", admin_port)
await self.admin_runner.setup()
admin_site = web.TCPSite(self.admin_runner, host, admin_port)
await admin_site.start()
logger.info("Starting main server on port %d", port)
await self.runner.setup()
site = web.TCPSite(self.runner, host, port)
await site.start()
while True:
await asyncio.sleep(3600)
def run(self, args: Namespace) -> None:
"""
Run the server.
@ -1408,11 +1502,14 @@ class Server:
- `registration_keyfile`, the file containing the registration keys
- `private`, if the server is private
- `restricted`, if the server is restricted
- `admin_port`, the port for the admin interface
:param args: The command line arguments
:type args: Namespace
:rtype: None
"""
logger.setLevel(logging.INFO)
self.app["type"] = (
"private" if args.private else "restricted" if args.restricted else "public"
)
@ -1428,12 +1525,25 @@ class Server:
self.app.router.add_route("*", "/", self.root_handler)
self.app.router.add_route("*", "/{room}", self.root_handler)
self.app.router.add_route("*", "/{room}/", self.root_handler)
self.admin_app.router.add_route("*", "/", self.admin_handler)
self.app.cleanup_ctx.append(self.background_tasks)
if args.admin_password:
self.sio.instrument(auth={"username": "admin", "password": args.admin_password})
web.run_app(self.app, host=args.host, port=args.port)
try:
asyncio.run(
self.run_apps(
args.host,
args.port,
args.admin_port,
)
)
except KeyboardInterrupt:
logger.info("Shutting down server...")
asyncio.run(self.runner.cleanup())
asyncio.run(self.admin_runner.cleanup())
logger.info("Server shut down.")
def run_server(args: Namespace) -> None:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -5,8 +5,8 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syng Rocks!</title>
<script type="module" crossorigin src="/assets/index.37d42915.js"></script>
<link rel="stylesheet" href="/assets/index.398fca41.css">
<script type="module" crossorigin src="/assets/index.955e32a1.js"></script>
<link rel="stylesheet" href="/assets/index.55d5e0aa.css">
</head>
<body>
<div id="app"></div>

View file

@ -11,7 +11,13 @@ class _session_context_manager:
async def __aenter__(self) -> dict[str, Any]: ...
async def __aexit__(self, *args: list[Any]) -> None: ...
class Manager:
rooms: dict[str, set[str]]
def get_namespaces(self) -> list[str]: ...
def get_participants(self, namespace: str, room: str) -> list[tuple[str, str]]: ...
class AsyncServer:
manager: Manager
def __init__(
self,
cors_allowed_origins: str,