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,