ytptube/app/library/HttpSocket.py

127 lines
4 KiB
Python

import functools
import logging
from pathlib import Path
from typing import Any
import socketio
from aiohttp import web
from app.library.router import RouteType, get_routes
from app.library.Services import Services
from app.library.Utils import load_modules
from .config import Config
from .DownloadQueue import DownloadQueue
from .encoder import Encoder
from .Events import Event, EventBus, Events
from .ItemDTO import Item
LOG: logging.Logger = logging.getLogger("socket_api")
class HttpSocket:
"""
This class is used to handle WebSocket events.
"""
config: Config
sio: socketio.AsyncServer
queue: DownloadQueue
di_context: dict[str, Any] = {}
def __init__(
self,
root_path: Path,
queue: DownloadQueue | None = None,
encoder: Encoder | None = None,
config: Config | None = None,
sio: socketio.AsyncServer | None = None,
):
self.config = config or Config.get_instance()
self.queue = queue or DownloadQueue.get_instance()
self._notify = EventBus.get_instance()
self.sio = sio or socketio.AsyncServer(
async_handlers=True,
async_mode="aiohttp",
cors_allowed_origins=[],
transports=["websocket", "polling"],
logger=self.config.debug,
engineio_logger=self.config.debug,
ping_interval=10,
ping_timeout=5,
)
encoder = encoder or Encoder()
self.rootPath: Path = root_path
async def event_handler(e: Event, _, **kwargs):
await self.sio.emit(event=e.event, data=encoder.encode(e), **kwargs)
services = Services.get_instance()
services.add_all(
{
k: v
for k, v in {
"config": self.config,
"queue": self.queue,
"sio": self.sio,
"encoder": encoder,
"notify": self._notify,
"root_path": self.rootPath,
}.items()
if not services.has(k)
}
)
self._notify.subscribe("frontend", event_handler, f"{__class__.__name__}.emit")
@staticmethod
def ws_event(func): # type: ignore
"""
Decorator to mark a method as a socket event.
"""
@functools.wraps(func) # type: ignore
async def wrapper(*args, **kwargs):
return await func(*args, **kwargs) # type: ignore
wrapper._ws_event = func.__name__ # type: ignore
return wrapper
async def on_shutdown(self, _: web.Application):
LOG.debug("Shutting down socket server.")
for sid in self.sio.manager.get_participants("/", None):
LOG.debug(f"Disconnecting client '{sid}'.")
await self.sio.disconnect(sid[0], namespace="/")
LOG.debug("Socket server shutdown complete.")
def attach(self, app: web.Application):
app.on_shutdown.append(self.on_shutdown)
self.sio.attach(app, socketio_path=f"{self.config.base_path.rstrip('/')}/socket.io")
async def event_handler(data: Event, _):
if data and data.data:
await self.queue.add(item=Item.format(data.data))
self._notify.subscribe(Events.ADD_URL, event_handler, f"{__class__.__name__}.add")
load_modules(self.rootPath, self.rootPath / "routes" / "socket")
for route in get_routes(RouteType.SOCKET).values():
if self.config.debug:
LOG.debug(
f"Add ({route.name}) {route.method.value if isinstance(route.method, RouteType) else route.method}: {route.path}."
)
self.sio.on(route.path)(HttpSocket._injector(route.handler, route.path))
@staticmethod
def _injector(func, event: str):
async def wrapper(sid, data=None, **kwargs):
if not data:
data = {}
return await Services.get_instance().handle_async(func, sid=sid, data=data, event=event, **kwargs)
return wrapper