6500d6f615
Lint & Test / test (push) Successful in 9s
Adds pystray-based tray icon (green play button) that runs alongside uvicorn. Double-click opens the web UI in the browser, Exit triggers graceful shutdown. Disabled with --no-tray flag for headless/service mode.
102 lines
2.5 KiB
Python
102 lines
2.5 KiB
Python
"""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
|