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,
|
||||
|
||||
Reference in New Issue
Block a user