"""System tray icon for Media Server.""" import io import logging import os import signal import threading import webbrowser from PIL import Image, ImageDraw logger = logging.getLogger(__name__) # pystray is optional — tray silently disabled when missing try: import pystray except ImportError: pystray = None def _create_icon_image(size: int = 64) -> Image.Image: """Create a tray icon: green circle with white play triangle.""" img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Green circle background padding = 2 draw.ellipse( [padding, padding, size - padding, size - padding], fill=(29, 185, 84, 255), ) # White play triangle cx, cy = size // 2, size // 2 r = size * 0.28 triangle = [ (cx - r * 0.6, cy - r), (cx - r * 0.6, cy + r), (cx + r * 0.9, cy), ] draw.polygon(triangle, fill=(255, 255, 255, 255)) return img def _load_icon_image() -> Image.Image: """Load the SVG app icon, falling back to a generated image.""" try: import cairosvg svg_path = os.path.join( os.path.dirname(__file__), "static", "icons", "icon.svg" ) if os.path.exists(svg_path): png_data = cairosvg.svg2png(url=svg_path, output_width=64, output_height=64) return Image.open(io.BytesIO(png_data)) except Exception: pass return _create_icon_image() def start_tray(host: str, port: int) -> "pystray.Icon | None": """Start system tray icon in a background thread. Returns the Icon instance (call icon.stop() to remove), or None if pystray is not installed. """ if pystray is None: logger.info("pystray not installed — tray icon disabled") return None url = f"http://{'localhost' if host == '0.0.0.0' else host}:{port}" def on_show_ui(_icon, _item): webbrowser.open(url) def on_exit(_icon, _item): logger.info("Exit requested from tray") _icon.stop() # Signal the main process to shut down gracefully os.kill(os.getpid(), signal.SIGINT) menu = pystray.Menu( pystray.MenuItem("Show UI", on_show_ui, default=True), pystray.Menu.SEPARATOR, pystray.MenuItem("Exit", on_exit), ) icon = pystray.Icon( name="media-server", icon=_load_icon_image(), title="Media Server", menu=menu, ) thread = threading.Thread(target=icon.run, daemon=True) thread.start() logger.info("System tray icon started") return icon