Backend optimizations: - GZip middleware for compressed responses - Concurrent WebSocket broadcast - Skip status polling when no clients connected - Deduplicated token validation with caching - Fire-and-forget HA state callbacks - Single stat() per browser item - Metadata caching (LRU) - M3U playlist optimization - Autostart setup (Task Scheduler + hidden VBS launcher) Frontend code optimizations: - Fix thumbnail blob URL memory leak - Fix WebSocket ping interval leak on reconnect - Skip artwork re-fetch when same track playing - Deduplicate volume slider logic - Extract magic numbers into named constants - Standardize error handling with toast notifications - Cache play/pause SVG constants - Loading state management for async buttons - Request deduplication for rapid clicks - Cache 30+ DOM element references - Deduplicate volume updates over WebSocket Frontend design improvements: - Progress bar seek thumb and hover expansion - Custom themed scrollbars - Toast notification accent border strips - Keyboard focus-visible states - Album art ambient glow effect - Animated sliding tab indicator - Mini-player top progress line - Empty state SVG illustrations - Responsive tablet breakpoint (601-900px) - Horizontal player layout on wide screens (>900px) - Glassmorphism mini-player with backdrop blur - Vinyl spin animation (toggleable) - Table horizontal scroll on narrow screens Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
189 lines
5.5 KiB
Python
189 lines
5.5 KiB
Python
"""Media Server - FastAPI application entry point."""
|
|
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
import uvicorn
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.middleware.gzip import GZipMiddleware
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from . import __version__
|
|
from .auth import get_token_label, token_label_var
|
|
from .config import settings, generate_default_config, get_config_dir
|
|
from .routes import audio_router, browser_router, callbacks_router, health_router, media_router, scripts_router
|
|
from .services import get_media_controller
|
|
from .services.websocket_manager import ws_manager
|
|
|
|
|
|
class TokenLabelFilter(logging.Filter):
|
|
"""Add token label to log records."""
|
|
|
|
def filter(self, record):
|
|
record.token_label = token_label_var.get("unknown")
|
|
return True
|
|
|
|
|
|
def setup_logging():
|
|
"""Configure application logging with token labels."""
|
|
# Create filter and handler
|
|
token_filter = TokenLabelFilter()
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
handler.addFilter(token_filter)
|
|
|
|
logging.basicConfig(
|
|
level=getattr(logging, settings.log_level.upper()),
|
|
format="%(asctime)s - %(name)s - [%(token_label)s] - %(levelname)s - %(message)s",
|
|
handlers=[handler],
|
|
)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Application lifespan handler."""
|
|
setup_logging()
|
|
logger = logging.getLogger(__name__)
|
|
logger.info(f"Media Server starting on {settings.host}:{settings.port}")
|
|
|
|
# Log all configured tokens
|
|
for label, token in settings.api_tokens.items():
|
|
logger.info(f"API Token [{label}]: {token[:8]}...")
|
|
|
|
# Start WebSocket status monitor
|
|
controller = get_media_controller()
|
|
await ws_manager.start_status_monitor(controller.get_status)
|
|
logger.info("WebSocket status monitor started")
|
|
|
|
yield
|
|
|
|
# Stop WebSocket status monitor
|
|
await ws_manager.stop_status_monitor()
|
|
logger.info("Media Server shutting down")
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
"""Create and configure the FastAPI application."""
|
|
app = FastAPI(
|
|
title="Media Server",
|
|
description="REST API for controlling system media playback",
|
|
version=__version__,
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
# Compress responses > 1KB
|
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
|
|
|
# Add CORS middleware for cross-origin requests
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Add token logging middleware
|
|
@app.middleware("http")
|
|
async def token_logging_middleware(request: Request, call_next):
|
|
"""Extract token label and set in context for logging."""
|
|
token_label = "unknown"
|
|
|
|
# Try Authorization header
|
|
auth_header = request.headers.get("authorization", "")
|
|
if auth_header.startswith("Bearer "):
|
|
token = auth_header[7:]
|
|
label = get_token_label(token)
|
|
if label:
|
|
token_label = label
|
|
|
|
# Try query parameter (for artwork endpoint)
|
|
elif "token" in request.query_params:
|
|
token = request.query_params["token"]
|
|
label = get_token_label(token)
|
|
if label:
|
|
token_label = label
|
|
|
|
token_label_var.set(token_label)
|
|
response = await call_next(request)
|
|
return response
|
|
|
|
# Register routers
|
|
app.include_router(audio_router)
|
|
app.include_router(browser_router)
|
|
app.include_router(callbacks_router)
|
|
app.include_router(health_router)
|
|
app.include_router(media_router)
|
|
app.include_router(scripts_router)
|
|
|
|
# Mount static files and serve UI at root
|
|
static_dir = Path(__file__).parent / "static"
|
|
if static_dir.exists():
|
|
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
|
|
|
@app.get("/", include_in_schema=False)
|
|
async def serve_ui():
|
|
"""Serve the Web UI."""
|
|
return FileResponse(static_dir / "index.html")
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|
|
|
|
|
|
def main():
|
|
"""Main entry point for running the server."""
|
|
parser = argparse.ArgumentParser(description="Media Server")
|
|
parser.add_argument(
|
|
"--host",
|
|
default=settings.host,
|
|
help=f"Host to bind to (default: {settings.host})",
|
|
)
|
|
parser.add_argument(
|
|
"--port",
|
|
type=int,
|
|
default=settings.port,
|
|
help=f"Port to bind to (default: {settings.port})",
|
|
)
|
|
parser.add_argument(
|
|
"--generate-config",
|
|
action="store_true",
|
|
help="Generate a default configuration file and exit",
|
|
)
|
|
parser.add_argument(
|
|
"--show-token",
|
|
action="store_true",
|
|
help="Show the current API token and exit",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.generate_config:
|
|
config_path = generate_default_config()
|
|
print(f"Configuration file generated at: {config_path}")
|
|
print(f"API Token has been saved to the config file.")
|
|
return
|
|
|
|
if args.show_token:
|
|
print(f"Config directory: {get_config_dir()}")
|
|
print(f"\nAPI Tokens:")
|
|
for label, token in settings.api_tokens.items():
|
|
print(f" {label:20} {token}")
|
|
return
|
|
|
|
uvicorn.run(
|
|
"media_server.main:app",
|
|
host=args.host,
|
|
port=args.port,
|
|
reload=False,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|