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.
This commit is contained in:
+22
-6
@@ -216,6 +216,11 @@ def main():
|
||||
action="store_true",
|
||||
help="Show the current API token and exit",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-tray",
|
||||
action="store_true",
|
||||
help="Disable system tray icon (for headless/service mode)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -235,12 +240,23 @@ def main():
|
||||
print("\nAuthentication is DISABLED (no tokens configured)")
|
||||
return
|
||||
|
||||
uvicorn.run(
|
||||
"media_server.main:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=False,
|
||||
)
|
||||
# Start system tray icon (unless disabled)
|
||||
tray_icon = None
|
||||
if not args.no_tray:
|
||||
from .tray import start_tray
|
||||
|
||||
tray_icon = start_tray(args.host, args.port)
|
||||
|
||||
try:
|
||||
uvicorn.run(
|
||||
"media_server.main:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=False,
|
||||
)
|
||||
finally:
|
||||
if tray_icon is not None:
|
||||
tray_icon.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""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
|
||||
@@ -42,6 +42,7 @@ windows = [
|
||||
"pycaw>=20230407",
|
||||
"screen-brightness-control>=0.20.0",
|
||||
"monitorcontrol>=3.0.0",
|
||||
"pystray>=0.19.0",
|
||||
]
|
||||
visualizer = [
|
||||
"soundcard>=0.4.0",
|
||||
|
||||
Reference in New Issue
Block a user