import logging import os import socket import socketserver import time from croaker.gui import GUI from croaker.path import playlist_root from croaker.playlist import load_playlist logger = logging.getLogger(__name__) class RequestHandler(socketserver.StreamRequestHandler): """ Instantiated by the TCPServer when a request is received. Implements the command and control protocol and issues commands to the GUI application. """ supported_commands = { # command # help text "PLAY": "PLAYLIST - Switch to the specified playlist.", "LIST": "[PLAYLIST] - List playlists or contents of the specified list.", "FFWD": " - Skip to the next track in the playlist.", "HELP": " - Display command help.", "KTHX": " - Close the current connection.", "STOP": " - Stop the current track and stream silence.", "STFU": " - Terminate the Croaker server.", } should_listen = True def handle(self): """ Start a command and control session. Commands are read one line at a time; the format is: Byte Definition ------------------- 0-3 Command 4 Ignored 5+ Arguments """ while self.should_listen: time.sleep(0.01) self.data = self.rfile.readline().strip().decode() logger.debug(f"Received: {self.data}") try: cmd = self.data[0:4].strip().upper() if not cmd: continue elif cmd not in self.supported_commands: self.send(f"ERR Unknown Command '{cmd}'") except IndexError: self.send(f"ERR Command not understood '{cmd}'") continue args = self.data[5:] if cmd == "KTHX": return self.send("KBAI") handler = getattr(self, f"handle_{cmd}", None) if not handler: self.send(f"ERR No handler for {cmd}.") continue handler(args) def send(self, msg): return self.wfile.write(msg.encode() + b"\n") def handle_PLAY(self, args): self.server.player.load(args) return self.send("OK") def handle_BACK(self, args): self.server.player.back_requested.set() return self.send("OK") def handle_FFWD(self, args): self.server.player.ffwd_requested.set() return self.send("OK") def handle_LIST(self, args): return self.send(self.server.list(args)) def handle_HELP(self, args): return self.send("\n".join(f"{cmd} {txt}" for cmd, txt in self.supported_commands.items())) def handle_STOP(self, args): return self.server.player.stop_requested.set() def handle_STFU(self, args): self.send("Shutting down.") self.server.shutdown() class Controller(socketserver.TCPServer): """ A TCP Server that listens for commands and proxies the GUI audio player. """ def __init__(self, player: GUI): self.player = player super().__init__((os.environ["HOST"], int(os.environ["PORT"])), RequestHandler) def server_bind(self): self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) def shutdown(self): self.player.shutdown_requested.set() exit() def list(self, playlist_name: str = None): if playlist_name: return str(load_playlist(playlist_name)) return "\n".join([str(p.name) for p in playlist_root().iterdir()])