- Add `linux` (dbus-python, PyGObject, python-xlib) and `macos`
(pyobjc) extras to pyproject.toml with sys_platform markers; move
cross-platform screen-brightness-control + monitorcontrol to base deps.
- build-dist-linux.sh: install `.[linux]`, pkg-config pre-flight for
dbus-1/glib-2.0, emit a systemd unit with DBUS_SESSION_BUS_ADDRESS +
XDG_RUNTIME_DIR + ReadWritePaths for ~/.config and ~/.cache so MPRIS
works and audit-log / thumbnail writes aren't blocked by ProtectHome.
- New build-dist-macos.sh + per-user LaunchAgent installer producing
MediaServer-vX.Y-macos-{arm64,x86_64}.tar.gz.
- Templated media-server.service updated to match the dist layout with
proper session-bus env vars and a writable state-dir grant.
- install_linux.sh: drop dead requirements.txt path; install via
`pip install ".[linux]"` and pre-create the writable state dirs.
- Cross-platform album artwork: abstract MediaController.get_album_art()
with Linux (mpris:artUrl, file:// + http(s)://) and macOS (Spotify URL)
impls; routes/media artwork endpoint now awaits the controller.
- LinuxMediaController connects to the session bus lazily — failure no
longer crashes lifespan startup; MPRIS calls return idle until the bus
is reachable. Logged once at INFO with a hint about
`loginctl enable-linger`.
- Startup preflight on Linux warns if DBUS_SESSION_BUS_ADDRESS or
XDG_RUNTIME_DIR is unset and informs the user when Wayland disables
the foreground probe.
- /api/media/visualizer/status now reports a per-OS unavailable_reason.
- tray._confirm guarded against ctypes.windll on non-Windows.
- config.example.yaml: per-OS commented script examples; on_turn_off
default is now a no-op echo (used to silently fail off Windows).
- README: replace stale `pip install -r requirements.txt` instructions
with the new extras; add systemd lingering doc + troubleshooting
section; add macOS LaunchAgent section.
- CI: new linux-smoke job (installs `.[linux]`, boots the server under
dbus-run-session, asserts /api/health). Release workflow gains
apt-deps step for the Linux build and a best-effort macOS build job.
When `cors_origins` was unset, the WS endpoint only allowed
`http://localhost:<port>` and `http://127.0.0.1:<port>` as origins, so a
browser opening the UI via the LAN IP (e.g. `http://192.168.2.100:8765`
when bound to `0.0.0.0`) had its WebSocket closed with code 4003 and
never recovered — leaving the Web UI in a permanent reconnect loop.
Also accept any `Origin` whose authority matches the request's `Host`
header (both `http://` and `https://` schemes). Same-origin is by
definition not CSWSH, so the cross-origin defence added in v0.3.0
remains intact for genuine third-party LAN pages.
Security
- Default scripts_management, callbacks_management, links_management, and
media_folders_management to False so a leaked token cannot escalate to RCE
through admin CRUD endpoints.
- TokenSpec + scope hierarchy (read | control | admin); legacy bare-string
api_tokens entries promote to admin for back-compat. Management endpoints
now require admin scope.
- WebSocket subprotocol auth (Sec-WebSocket-Protocol: media-server.token.<T>)
preferred over ?token= query so the token no longer lands in URL/history/
Referer; query fallback retained for HA integration back-compat.
- Origin allow-list check on the WS endpoint (CSWSH defence).
- In-process token-bucket rate limiter: 5/min for failed auths,
10/min for /api/scripts/execute and /api/callbacks/execute.
- shell=False subprocess path (shlex.split) + per-parameter regex `pattern`
in ScriptParameterConfig to harden shell=true scripts against parameter
injection (Windows cmd.exe env-var expansion).
- CSP gains form-action, worker-src, manifest-src directives.
- Refuse cors_origins=["*"] at startup; strip token=... from uvicorn access
logs; validate Gitea release tag against strict SemVer regex.
- noopener noreferrer + no-referrer referrerpolicy on every outbound link.
- icacls hardening of config.yaml on Windows (current user + SYSTEM +
Administrators only); 0600 still enforced on POSIX.
- WS volume handler clamps input and never drops the socket on bad messages.
Performance
- Album-art read in windows_media gated by track key — was decoding the
WinRT thumbnail twice per second regardless of track changes.
- /api/media/artwork returns content-derived ETag + Cache-Control so the
browser sends If-None-Match and gets 304s on track repeats.
- Foreground-service ctypes argtypes hoisted to one-time module init
(was re-declaring ~14 prototypes per probe).
- display_service _static_cache keyed by (edid_hash, ...) tuple with
eviction of disappeared monitors — fixes stale capabilities on hot-plug
swaps where the new topology has the same monitor count.
- Visualizer rAF loop paused on document.hidden, resumed on visible.
Reliability / bug fixes
- Lifespan rewritten as try/yield/finally so a partial-startup failure
cannot orphan background tasks or executors.
- _run_callback in routes/media.py keeps a strong task ref (GC-safe) and
uses the dedicated callback executor instead of the default pool.
- macos_media.set_volume() no longer always returns True.
- TrayManager._restart_requested initialised in __init__; set before
signalling exit so the main thread observes it correctly.
- Missing static_dir now logs a WARNING instead of silent UI disable.
UX / accessibility / PWA
- manifest.json theme_color and background_color match the Studio Reference
base (#0E0D0B); added id and scope for PWA installability.
- ARIA on mini-player icon buttons; inner SVGs marked aria-hidden.
- OS mediaSession API wired so headset / lockscreen / Bluetooth buttons
drive play/pause/next/prev/seek and show track metadata + artwork.
Observability
- X-Request-ID middleware (accept upstream id if it matches a safe regex,
otherwise UUID4); request_id_var added to ContextVars and included in
every log line alongside the token label.
- Audit log (append-only JSONL) for every script + callback execution,
including the on_play/on_pause/etc. event callbacks. Background-thread
writer; queue capped; flushed in lifespan teardown.
Deployment
- proxy_headers + forwarded_allow_ips plumbed through Settings →
uvicorn.Config for reverse-proxy installs.
- HTTPS support via ssl_certfile + ssl_keyfile (+ optional password);
startup refuses to launch with only one of the pair set.
- Thumbnail cache moved from project-root .cache to
%LOCALAPPDATA%/media-server/cache (Windows) and
$XDG_CACHE_HOME/media-server/thumbnails (POSIX).
Tests
- 35 new tests across auth scopes, rate limiter, browser path traversal
(../ NUL UNC absolute), script-param validation incl. regex, Gitea tag
whitelist, config atomic write + POSIX perms. 47 passed / 4 skipped.
Adds cross-platform foreground-window tracking and exposes it over REST
(/api/foreground) and the existing WebSocket feed.
- foreground_service.py: Windows probe via ctypes (HANDLE-correct argtypes
to avoid 64-bit handle truncation); macOS via AppKit; Linux via Xlib
(Wayland returns unavailable). TTL cache + per-platform fallback.
- browser_url_service.py: when foreground is a recognised browser, extract
the page title from the window title (browser-name suffix stripped) and
surface `is_browser` + `browser_page_title`. Optional UIA-based URL
extraction behind MEDIA_SERVER_BROWSER_UIA env flag (off by default —
Chromium browsers keep their accessibility tree dormant otherwise).
- websocket_manager: poll foreground every 1s inside the existing status
loop, broadcast `foreground` on connect and `foreground_update` on
change. Diff only on user-visible fields to avoid geometry spam.
- WebUI: new editorial card rendered under the monitor list on the
Display tab — process name, window title, fullscreen/minimized/monitor
chips, browser block when applicable, exe path, PID, started-ago,
geometry, platform. 16px inter-section gap matches Settings cadence.
- i18n: 25 new keys added to both en.json and ru.json.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Security
- Default bind 127.0.0.1; first-run bootstrap generates random api_token
and refuses to bind non-loopback without auth unless explicitly opted in
- Path-traversal hardened: BrowserService.validate_path rejects absolute
paths, drive letters, UNC, NUL bytes. /api/browser/{play,metadata,
thumbnail} now require folder_id and a folder-relative path
- Pydantic validators on links: http(s) URLs only, mdi:<slug> icons only
- Scripts/callbacks/links create/update/delete gated by *_management flags
- Strict CSP, X-Frame-Options DENY, Referrer-Policy no-referrer,
X-Content-Type-Options nosniff
- CORS locked to localhost:<port> + 127.0.0.1:<port> by default; configurable
- config.yaml writes atomic (tmp + os.replace) and 0o600 on POSIX
- Subprocesses spawned in their own process group / new session so timeout
kills the whole tree (Windows CREATE_NEW_PROCESS_GROUP, POSIX
start_new_session=True)
- Frontend XSS: monitor name + details escapeHtml'd; power button moved to
delegated data-action handler; remote MDI SVGs parsed and sanitized
(strip script/foreignObject/on*/javascript: hrefs) before innerHTML
- All dynamic URL segments now wrapped in encodeURIComponent
Bugs
- WebSocket reconnect: close previous socket before opening new, clear
ping interval per-socket, clear reconnectTimeout up-front, retry on
online/visibilitychange, try/catch JSON.parse
- Artwork fetch race: AbortController + generation guard
- _broadcast_after_open: initialize status, swallow per-poll errors,
background tasks tracked in a strong-ref set with done-callback cleanup
- Audio analyzer: sticky _unavailable flag prevents infinite start/stop
spin when no loopback device exists; cleared by set_device()
- Volume short-circuit cache invalidated when server reports remote volume
- Browser thumbnail race: per-folder generation counter + isConnected
checks; aborts in-flight fetches on navigation
- Track-skip uses cached title instead of full WinRT status round-trip
Performance
- Linux MPRIS/pactl and /api/display DDC-CI handlers wrapped in
asyncio.to_thread so blocking IO never stalls the event loop
- browse_directory moved off the event loop (SMB shares could freeze it)
- Windows status poll caches one asyncio loop per worker thread via
threading.local instead of new_event_loop/close on every 0.5s tick
- broadcast() serializes JSON once and uses send_text to all clients
- Hourly thumbnail cache cleanup scheduled in lifespan (was never invoked
— cache grew unbounded)
- Progress drag listeners attached only while dragging
Quality
- All asyncio.get_event_loop() in coroutines → get_running_loop()
- ThreadPoolExecutors shut down cleanly during lifespan teardown
- config_manager dedup: 12 near-identical methods collapsed onto generic
_upsert/_delete helpers (~290 lines removed)
- Service worker no longer pass-throughs every fetch
- M3U playlist written via NamedTemporaryFile (no fixed-path symlink
clobber race)
- __version__ now prefers live pyproject.toml in dev checkouts so
pip install -e . users see the source-of-truth version, not the stale
package-metadata version baked in at install time
UI/UX (Studio Reference)
- Green leftover focus rings (rgba(29,185,84,...)) all replaced with
copper accent (rgba(var(--copper-rgb),...))
- Dialogs: square corners, copper top hairline, unified with editorial
chrome
- .browser-item: transparent with copper hover border (was filled card)
- Audio device select uses var(--sans) instead of generic system font
- Mobile container padding tuned for ≤480px screens
- Breadcrumb home is a real <button> with aria-label; aria-current on root
- i18n: filled display.msg.power_*, execution.*, scripts.params.execute,
callbacks.empty in both en + ru
Backend (routes/display.py, services/display_service.py):
- Probe DDC/CI capabilities per monitor at enumeration time
- New endpoints POST /api/display/{contrast,input_source,color_preset,picture_mode}/{id}
- Picture mode goes through raw VCP 0xDC since monitorcontrol has no
high-level wrapper; labels follow MCCS spec with vendor-friendly fallbacks
- Each capability reports a *_supported flag so the UI can hide rows that
the hardware does not advertise
Frontend (links.js, app.js, styles.css, locales):
- Monitor cards grow a contrast slider (same editorial copper treatment
as brightness) and a "PICTURE TUNING" section beneath
- Picture tuning uses the IconSelect widget (matching the audio device
selector): per-port icons (HDMI, DisplayPort, DVI, VGA, USB-C),
thermometer for color temps, per-mode icons (movie reel, gamepad,
ball, etc.) for picture modes
- Humanizers turn SHOUT_CASE enum names into readable labels
(COLOR_TEMP_6500K -> "6500 K", HDMI1 -> "HDMI 1")
- 14 new i18n keys per locale (en/ru)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Abstract ReleaseProvider protocol for platform-agnostic version checking
- GiteaReleaseProvider implementation using stdlib urllib
- UpdateChecker service with periodic background checks and WS broadcast
- Persistent dismissible banner in Web UI when a new version is detected
- Health endpoint now returns cached update info
- Configurable via update_check_enabled and update_check_interval settings
- i18n support (EN/RU)
When no api_tokens are configured (the new default), all endpoints
are accessible without authentication. The frontend detects this
via /api/health's auth_required field and skips the login form.
- Backend: auth.py skips verification when api_tokens is empty
- Frontend: shared getAuthHeaders()/hasCredentials() helpers replace
scattered token logic across all JS modules
- Health endpoint exposes auth_required for frontend discovery
- config.example.yaml ships with tokens commented out
- CLI --show-token and startup log reflect disabled state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Audio capture starts only when first client subscribes,
stops when last client unsubscribes (saves CPU/battery)
- Add lifecycle lock to AudioAnalyzer for thread-safe start/stop
- Status badge uses local visualizer state instead of server flag
- Fix script name vertical text break on narrow screens
- Fix script grid minimum column width on small viewports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix CORS: set allow_credentials=False (token auth, not cookies)
- Add threading.Lock for position cache thread safety
- Add shutdown_executor() for clean ThreadPoolExecutor cleanup
- Dedicated ThreadPoolExecutors for script/callback execution
- Fix Mutagen file handle leaks with try/finally close
- Reduce idle WebSocket polling (0.5s → 2.0s when no clients)
- Add :focus-visible styles for playback control buttons
- Add aria-label to icon-only header buttons
- Dynamic album art alt text for screen readers
- Persist MDI icon cache to localStorage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New audio_analyzer service: loopback capture via soundcard + numpy FFT
- Real-time spectrogram bars below album art with accent color gradient
- Album art and vinyl pulse to bass energy beats
- WebSocket subscriber pattern for opt-in audio data streaming
- Audio device selection in Settings tab with auto-detect fallback
- Optimized FFT pipeline: vectorized cumsum bin grouping, pre-serialized JSON broadcast
- Visualizer config: enabled/fps/bins/device in config.yaml
- Optional deps: soundcard + numpy (graceful degradation if missing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Merge Scripts/Callbacks/Links tabs into single Settings tab with collapsible sections
- Rename Actions tab to Quick Access showing both scripts and configured links
- Add prev/next buttons to mini (secondary) player
- Add optional description field to links (backend + frontend)
- Add CSS chevron indicators on collapsible settings sections
- Persist section collapse/expand state in localStorage
- Fix race condition in Quick Access rendering with generation counter
- Order settings sections: Scripts, Links, Callbacks
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add LinkConfig model and links field to settings
- Add CRUD API endpoints for links (list/create/update/delete)
- Add Links management tab in WebUI with add/edit/delete dialogs
- Add live icon preview in Link and Script dialog forms
- Show MDI icons inline in Quick Actions cards, Scripts table, Links table
- Add broadcast_links_changed WebSocket event for live updates
- Add EN/RU translations for all links management strings
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- New display service with DDC/CI brightness and power control via screen_brightness_control and monitorcontrol
- New /api/display/* endpoints (monitors, brightness, power)
- Display tab in WebUI with per-monitor brightness sliders and power toggle
- EDID resolution parsing to distinguish same-name monitors
- Throttled brightness slider (50ms) matching volume control pattern
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add OSError/PermissionError handling in browse endpoint
- Return 503 status code when folders are temporarily unavailable
- Display user-friendly error messages in the UI
- Enhance error logging with exception type information
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Convert stacked sections to tabbed interface (Player, Browser, Actions, Scripts, Callbacks) with localStorage persistence
- Add in-memory directory listing cache (5-min TTL) with nocache bypass for refresh
- Defer stat()/duration calls to paginated items only for faster browse
- Move mini player from top to bottom with footer padding fix
- Always show scrollbar to prevent layout shift between tabs
- Add tab localization keys (en/ru)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add Play All button with M3U playlist generation (local temp file with absolute paths)
- Replace folder combobox with root folder cards and home icon breadcrumb
- Fix compact grid card sizing (64x64 thumbnails, align-items: start)
- Add loading spinner when browsing folders
- Cache browse items to avoid re-fetching on view mode switch
- Remove unused browser-controls CSS
- Add localization keys for Play All and Home (en/ru)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add browser UI with three view modes (grid, compact, list) and pagination
- Add file browsing, thumbnail loading, download, and play endpoints
- Add duration extraction via mutagen for media files
- Single-click plays media or navigates folders, with play overlay on hover
- Add type badges, file size display, and duration metadata
- Add localization keys for browser UI (en/ru)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Send volume updates through WebSocket instead of HTTP POST
- Reduce throttle from 50ms to 16ms (~60 updates/sec)
- Fall back to HTTP if WebSocket is disconnected
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add missing `except HTTPException: raise` in create_folder and
update_folder endpoints to prevent 400 errors being masked as 500s
- Fix install_task_windows.ps1 working directory to point to
media-server root instead of parent project root
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add version label next to Media Server header (fetched from /api/health)
- Move connection status dot before title, remove status text
- Move logout button into header after language selector
- Return 204 instead of 404 for missing thumbnails (eliminates console errors)
- Show "Title unavailable" when media is playing but title is empty
- Add player.title_unavailable translation key for en/ru locales
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Refactored index.html: Split into separate HTML (309 lines), CSS (908 lines), and JS (1,286 lines) files
- Implemented media browser with folder configuration, recursive navigation, and thumbnail display
- Added metadata extraction using mutagen library (title, artist, album, duration, bitrate, codec)
- Implemented thumbnail generation and caching with SHA256 hash-based keys and LRU eviction
- Added platform-specific file playback (os.startfile on Windows, xdg-open on Linux, open on macOS)
- Implemented path validation security to prevent directory traversal attacks
- Added smooth thumbnail loading with fade-in animation and loading spinner
- Added i18n support for browser (English and Russian)
- Updated dependencies: mutagen>=1.47.0, pillow>=10.0.0
- Added comprehensive media browser documentation to README
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Backend improvements:
- Add execution_time tracking for script execution
- Add execution_time tracking for callback execution
- Add /api/callbacks/execute/{callback_name} endpoint for debugging callbacks
Frontend improvements:
- Fix duration display showing N/A for fast scripts (0 is falsy in JS)
- Increase duration precision to 3 decimal places (0.001s)
- Always show output section with "(no output)" message when empty
- Improve output formatting with italic gray text for empty output
Documentation:
- Add localization section to README
- Document available languages (English, Russian)
- Add guide for contributing new translations
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Features:
- Runtime script CRUD operations (create, update, delete)
- Thread-safe ConfigManager for YAML updates
- WebSocket notifications for script changes
- Web UI script management interface with full CRUD
- Home Assistant auto-reload on script changes
- Client-side position interpolation for smooth playback
- Include command field in script list API response
Technical improvements:
- Added broadcast_scripts_changed() to WebSocket manager
- Enhanced HA integration to handle scripts_changed messages
- Implemented smooth position updates in Web UI (100ms interval)
- Thread-safe configuration updates with file locking
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Replace single api_token with api_tokens dict (label: token pairs)
- Add context-aware logging to track which client made each request
- Implement token label lookup with secure comparison
- Add logging middleware to inject token labels into request context
- Update logging format to display [label] in all log messages
- Fix WebSocket authentication to use new multi-token system
- Update CLI --show-token to display all tokens with labels
- Update config generation to use api_tokens format
- Update README with multi-token documentation
- Update config.example.yaml with multiple token examples
Benefits:
- Easy identification of clients in logs (Home Assistant, mobile, web UI, etc.)
- Per-client token management and revocation
- Better security and auditability
Example log output:
2026-02-06 03:36:20,806 - [home_assistant] - WebSocket client connected
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add CallbackConfig model for callback scripts
- Add callbacks section to config for optional command execution
- Add turn_on/turn_off/toggle endpoints (callback-only)
- Add callbacks for all media actions (play, pause, stop, next, previous, volume, mute, seek)
- Update README with callbacks documentation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
FastAPI REST API server for controlling system-wide media playback
on Windows, Linux, macOS, and Android.
Features:
- Play/Pause/Stop/Next/Previous track controls
- Volume control and mute
- Seek within tracks
- Current track info (title, artist, album, artwork)
- WebSocket real-time status updates
- Script execution API
- Token-based authentication
- Cross-platform support
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>