Backend optimizations, frontend optimizations, and UI design improvements
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>
This commit is contained in:
@@ -25,6 +25,28 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/browser", tags=["browser"])
|
||||
|
||||
|
||||
async def _broadcast_after_open(controller, label: str, max_wait: float = 2.0) -> None:
|
||||
"""Poll until media session registers, then broadcast status update.
|
||||
|
||||
Fires as a background task so the HTTP response returns immediately.
|
||||
"""
|
||||
try:
|
||||
interval = 0.3
|
||||
elapsed = 0.0
|
||||
while elapsed < max_wait:
|
||||
await asyncio.sleep(interval)
|
||||
elapsed += interval
|
||||
status = await controller.get_status()
|
||||
if status.state in ("playing", "paused"):
|
||||
break
|
||||
|
||||
status_dict = status.model_dump()
|
||||
await ws_manager.broadcast({"type": "status", "data": status_dict})
|
||||
logger.info(f"Broadcasted status update after opening: {label}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to broadcast status after opening {label}: {e}")
|
||||
|
||||
|
||||
# Request/Response Models
|
||||
class FolderCreateRequest(BaseModel):
|
||||
"""Request model for creating a media folder."""
|
||||
@@ -412,21 +434,8 @@ async def play_file(
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to open file")
|
||||
|
||||
# Wait for media player to start and register with Windows Media Session API
|
||||
# This allows the UI to update immediately with the new playback state
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
# Get updated status and broadcast to all connected clients
|
||||
try:
|
||||
status = await controller.get_status()
|
||||
status_dict = status.model_dump()
|
||||
await ws_manager.broadcast({
|
||||
"type": "status",
|
||||
"data": status_dict
|
||||
})
|
||||
logger.info(f"Broadcasted status update after opening file: {file_path.name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to broadcast status after opening file: {e}")
|
||||
# Poll until player registers with media session API (up to 2s)
|
||||
asyncio.create_task(_broadcast_after_open(controller, file_path.name))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -476,10 +485,11 @@ async def play_folder(
|
||||
# Generate M3U playlist with absolute paths and EXTINF entries
|
||||
# Written to local temp dir to avoid extra SMB file handle on network shares
|
||||
# Uses utf-8-sig (BOM) so players detect encoding properly
|
||||
m3u_content = "#EXTM3U\r\n"
|
||||
lines = ["#EXTM3U"]
|
||||
for f in media_files:
|
||||
m3u_content += f"#EXTINF:-1,{f.stem}\r\n"
|
||||
m3u_content += f"{f}\r\n"
|
||||
lines.append(f"#EXTINF:-1,{f.stem}")
|
||||
lines.append(str(f))
|
||||
m3u_content = "\r\n".join(lines) + "\r\n"
|
||||
|
||||
playlist_path = Path(tempfile.gettempdir()) / ".media_server_playlist.m3u"
|
||||
playlist_path.write_text(m3u_content, encoding="utf-8-sig")
|
||||
@@ -491,20 +501,8 @@ async def play_folder(
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to open playlist")
|
||||
|
||||
# Wait for media player to start
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
# Broadcast status update
|
||||
try:
|
||||
status = await controller.get_status()
|
||||
status_dict = status.model_dump()
|
||||
await ws_manager.broadcast({
|
||||
"type": "status",
|
||||
"data": status_dict
|
||||
})
|
||||
logger.info(f"Broadcasted status after opening playlist with {len(media_files)} files")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to broadcast status after opening playlist: {e}")
|
||||
# Poll until player registers with media session API (up to 2s)
|
||||
asyncio.create_task(_broadcast_after_open(controller, f"playlist ({len(media_files)} files)"))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
|
||||
@@ -18,31 +18,37 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/media", tags=["media"])
|
||||
|
||||
|
||||
async def _run_callback(callback_name: str) -> None:
|
||||
"""Run a callback if configured. Failures are logged but don't raise."""
|
||||
def _run_callback(callback_name: str) -> None:
|
||||
"""Fire-and-forget a callback if configured. Failures are logged but don't block."""
|
||||
if not settings.callbacks or callback_name not in settings.callbacks:
|
||||
return
|
||||
|
||||
from .scripts import _run_script
|
||||
async def _execute():
|
||||
from .scripts import _run_script
|
||||
|
||||
callback = settings.callbacks[callback_name]
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: _run_script(
|
||||
command=callback.command,
|
||||
timeout=callback.timeout,
|
||||
shell=callback.shell,
|
||||
working_dir=callback.working_dir,
|
||||
),
|
||||
)
|
||||
if result["exit_code"] != 0:
|
||||
logger.warning(
|
||||
"Callback %s failed with exit code %s: %s",
|
||||
callback_name,
|
||||
result["exit_code"],
|
||||
result["stderr"],
|
||||
)
|
||||
try:
|
||||
callback = settings.callbacks[callback_name]
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: _run_script(
|
||||
command=callback.command,
|
||||
timeout=callback.timeout,
|
||||
shell=callback.shell,
|
||||
working_dir=callback.working_dir,
|
||||
),
|
||||
)
|
||||
if result["exit_code"] != 0:
|
||||
logger.warning(
|
||||
"Callback %s failed with exit code %s: %s",
|
||||
callback_name,
|
||||
result["exit_code"],
|
||||
result["stderr"],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Callback %s error: %s", callback_name, e)
|
||||
|
||||
asyncio.create_task(_execute())
|
||||
|
||||
|
||||
@router.get("/status", response_model=MediaStatus)
|
||||
@@ -70,7 +76,7 @@ async def play(_: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to start playback - no active media session",
|
||||
)
|
||||
await _run_callback("on_play")
|
||||
_run_callback("on_play")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -88,7 +94,7 @@ async def pause(_: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to pause - no active media session",
|
||||
)
|
||||
await _run_callback("on_pause")
|
||||
_run_callback("on_pause")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -106,7 +112,7 @@ async def stop(_: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to stop - no active media session",
|
||||
)
|
||||
await _run_callback("on_stop")
|
||||
_run_callback("on_stop")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -124,7 +130,7 @@ async def next_track(_: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to skip - no active media session",
|
||||
)
|
||||
await _run_callback("on_next")
|
||||
_run_callback("on_next")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -142,7 +148,7 @@ async def previous_track(_: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to go back - no active media session",
|
||||
)
|
||||
await _run_callback("on_previous")
|
||||
_run_callback("on_previous")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -165,7 +171,7 @@ async def set_volume(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to set volume",
|
||||
)
|
||||
await _run_callback("on_volume")
|
||||
_run_callback("on_volume")
|
||||
return {"success": True, "volume": request.volume}
|
||||
|
||||
|
||||
@@ -178,7 +184,7 @@ async def toggle_mute(_: str = Depends(verify_token)) -> dict:
|
||||
"""
|
||||
controller = get_media_controller()
|
||||
muted = await controller.toggle_mute()
|
||||
await _run_callback("on_mute")
|
||||
_run_callback("on_mute")
|
||||
return {"success": True, "muted": muted}
|
||||
|
||||
|
||||
@@ -199,7 +205,7 @@ async def seek(request: SeekRequest, _: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to seek - no active media session or seek not supported",
|
||||
)
|
||||
await _run_callback("on_seek")
|
||||
_run_callback("on_seek")
|
||||
return {"success": True, "position": request.position}
|
||||
|
||||
|
||||
@@ -210,7 +216,7 @@ async def turn_on(_: str = Depends(verify_token)) -> dict:
|
||||
Returns:
|
||||
Success status
|
||||
"""
|
||||
await _run_callback("on_turn_on")
|
||||
_run_callback("on_turn_on")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -221,7 +227,7 @@ async def turn_off(_: str = Depends(verify_token)) -> dict:
|
||||
Returns:
|
||||
Success status
|
||||
"""
|
||||
await _run_callback("on_turn_off")
|
||||
_run_callback("on_turn_off")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -232,7 +238,7 @@ async def toggle(_: str = Depends(verify_token)) -> dict:
|
||||
Returns:
|
||||
Success status
|
||||
"""
|
||||
await _run_callback("on_toggle")
|
||||
_run_callback("on_toggle")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user