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:
2026-02-23 20:38:35 +03:00
parent d1ec27cb7b
commit 84b985e6df
13 changed files with 926 additions and 348 deletions

View File

@@ -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,

View File

@@ -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}