- 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.
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
DDC/CI writes are fire-and-forget at the protocol level: a successful send
does not mean the monitor honored the value. Many monitors (LG ultrawides
in particular) silently drop writes for VCP codes whose registers exist
but whose feature isn't really implemented in firmware.
- New _verify_after_set helper polls readback after every DDC/CI write and
reports {success: false} when the monitor didn't apply the value. Wired
into set_contrast, set_input_source, set_color_preset, set_picture_mode.
Input source uses a longer settle window since switching can briefly
disrupt the DDC/CI link.
- Picture mode (VCP 0xDC) now requires the capability string to declare
supported codes under cmds[0xDC]. Without that declaration we treat the
feature as unsupported even when reads succeed - the LG case where reads
return a stuck value and every write is silently ignored.
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>
Frontend hot path (player.js, background.js):
- visualizer rAF: drop per-frame getComputedStyle('--accent') (cached on
applyAccentColor), build canvas LinearGradient once per accent change
instead of 32× per frame, batch all bars into a single beginPath/fill
- FPS-gate canvas redraw via frequencyDataVersion so 60-144 Hz monitors
stop re-rendering identical frames produced at 30 Hz on the backend
- editorial spectrum bars: replace style.height (layout) with
transform: scaleY (compositor-only); cache bar refs, pre-compute
per-bar gain/range, dedup writes at 1/1000 quantization
- coalesce VU needle into the visualizer rAF; cache vuNeedle ref;
dedup angle writes at 0.1°
- updateUI: status-payload fingerprint short-circuits the redundant
status_update broadcasts that fire during a track change
- swapArtworkSrc: only force layout reflow when keyframe is in flight;
drop the ?_=Date.now() cache-buster so identical artwork URLs reuse
the decoded bitmap; mini/glow imgs only re-set src when changed
- drop the fullscreen MutationObserver — fs-bloom-art is mirrored
directly from the artwork-swap path, eliminating the second blur paint
- updateProgress: skip text writes when the rounded second hasn't moved;
POSITION_INTERPOLATION_MS 100 → 250
- background.js: lift resizeBackgroundCanvas out of the rAF body, cache
step, accept new int-scaled wire format
CSS:
- spectrum bars use transform: scaleY(var(--bar-h-scale)) + transition
on transform; will-change updated to transform
- album-art-glow and fs-bloom-art switched to small-source-blur trick
(render at 20-25% size, scale 4-6×, lower blur radius) — visually
equivalent, ~10-25× cheaper repaint on track change
- drop unused transition: filter on .vinyl-stage #album-art
Backend (audio_analyzer.py, websocket_manager.py):
- pre-allocate windowed and cumsum buffers; replace
np.concatenate(([0.0], np.cumsum(...))) with cumsum[0]=0 +
np.cumsum(out=cumsum[1:]); float32 hanning window
- RMS via np.dot(mono, mono) — no astype copy, no ** temp
- int16 wire format (scale=1000) — smaller JSON, no Python float boxing
- versioned data + threading.Event so _audio_broadcast_loop is event-
driven (ev.wait + monotonic seq dedup) instead of polling on a timer
with the always-false `data is _last_data` identity check
ruff clean, pytest 7 passed / 3 numpy-skipped, esbuild bundle 113.6 kB.
Production-readiness pass before merging the Studio Reference redesign
to master.
Audio (backend):
- Reset AGC `_spectrum_ref` envelope on `start()` so a long silent gap
between sessions doesn't make the first new transients clip at the
ceiling. Annotated the trade-off (loud transient lifts reference for
a few seconds afterwards — the price of real loudness).
- Add `tests/test_audio_analyzer.py` with 10 cases: bin-edge layout,
AGC attack/release asymmetry, lifecycle reset. Skips numpy-dependent
cases when numpy isn't installed; CI has it.
Vinyl mode dead code removed:
- The toggle button was dropped during the sleeve refactor but the JS
state, 2 s `setInterval`, `beforeunload` handler, and `applyVinylMode`
call (commented out in app.js) all stayed. Now properly excised from
player.js + app.js + window.* exports.
- Stripped the matching `.album-art-container.vinyl*` CSS block and its
`vinylSpin` keyframes (~95 LoC).
Sleeve + tonearm fixes:
- Removed the duplicate `.now-playing .vinyl-stage` / `.vinyl-label` /
`.tonearm` block that was overriding the new `.vinyl-stage` rules by
source order — the uncommitted tonearm geometry never took effect
because the stale clone won the cascade.
- Tightened tonearm to 36% × 36% at right:-6%, top:26% so the SVG
bounding box stays right of the sleeve (sleeve right edge ~68%).
Needle now lands on the visible disc grooves at both rest and
playing rotations and never overlaps the cover.
- Removed sleeve `transform: rotate(-2.5deg)` + the matching mobile
`-1.8deg` override; sleeve now sits flat and squared-off.
- Removed the 1px inset hairline on the sleeve and the 1px outline +
inset highlight on the album art — cleaner, no semitransparent
border noise.
- Album art inset 5% to expose a cardstock margin around the print
(using explicit width/height — `inset` shorthand triggered the CSS
replaced-element rule that uses the image's intrinsic size and blew
out the grid track).
Mobile + misc:
- Removed mobile tonearm overrides at 720px and 420px — they were
calibrated for the pre-sleeve geometry and put the needle back over
the cover on phones; desktop geometry is proportional and works.
- Added `<meta name="mobile-web-app-capable">` alongside the legacy
Apple variant to silence the deprecation warning in Chromium.
- Replaced the "PRIMARY" badge on display cards with a copper star
icon (translation key still drives title + aria-label).
- `.gitattributes` with `* text=auto eol=lf` so Windows checkouts stop
nagging "LF will be replaced by CRLF".
Annotations:
- "REF · 24" record-label catalogue mark marked as intentional non-i18n
decoration in index.html.
CI: ruff clean, pytest 7 passed + 3 numpy-skipped (all 10 run on CI).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Backend computes time-domain RMS, maps -60..-6 dB to 0..1, sends as
`level` alongside the per-frame-normalized frequency bins.
- Frontend prefers `level` directly; drops the peak-of-bins fallback
and the redundant volume-slider attenuation (loopback capture is
already post-volume on Windows/macOS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Generate numpy/_distributor_init_local.py during build so libopenblas
can be located when running from the Windows installer
- Add os.add_dll_directory() call at runtime as a fallback for embedded Python
- Broaden audio import errors from ImportError to Exception, log at warning
- Move visualizer WS re-subscription into loadAudioDevices() so it runs
after availability is confirmed from the API
- Show/hide the visualizer toggle button based on fetched availability
Instead of waiting for the next poll cycle, new clients now get the
current playback status immediately on connect by calling get_status_func
if no cached status is available yet.
- Rename GITEA_TOKEN to DEPLOY_TOKEN in release workflow
- Extract shared version detection into build-common.sh
- Use importlib.metadata for runtime version instead of hardcoded string
- Use PEP 440 parsing (packaging lib) for update version comparison
- Add packaging>=23.0 to dependencies
- Fix update banner close button alignment (CSS)
- Update CLAUDE.md with versioning docs and frontend rebuild notes
- 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)
- Visualizer: FPS 25→30, chunk_size 2048→1024, smoothing 0.65→0.15
- Beat effect: scale 0.03→0.04, glow range 0.5-0.8→0.4-0.8
- UI: reduce container/section paddings from 2rem to 1rem
- Source name: add ellipsis overflow for long names
- Mobile browser toolbar: use flex-wrap instead of column stack,
hide "Items per page" label text on small screens
Co-Authored-By: Claude Opus 4.6 <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>
- Detect primary monitor via Windows EnumDisplayMonitors API and show badge
- Expand accent color picker with 9 presets and custom color input
- Auto-generate hover color for custom accent colors
- Re-render accent swatches on locale change for proper i18n
- Replace restart-server.bat with PowerShell restart-server.ps1
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>
- Extract title and artist tags via mutagen easy=True in get_media_info
- Display "Artist – Title" in both grid and list views, fall back to filename
- Show original filename in tooltip on hover
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract bitrate alongside duration in browse_directory via get_media_info
- Display bitrate in large card view metadata (duration · bitrate · size)
- Replace Audio/Video type badge with bitrate column in list view
- Remove Play All button text, keep icon only
- Add formatBitrate helper function
Co-Authored-By: Claude Opus 4.6 <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 refresh button to browser toolbar to re-fetch current folder
- Cache "no thumbnail" results to avoid repeated slow SMB lookups
- Fix list view fallback icon sizing for files without album art
- Fix view toggle button hover (no background/scale on hover)
- Skip re-render when clicking already-active view mode button
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>
- 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>
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>
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>