The strict `script-src 'self'` CSP blocks inline onclick/onchange/oninput/
onsubmit attribute evaluation, breaking every button and form in the UI.
- Rename all 53 inline handler attributes in index.html to data-on*
- Add wireInlineHandlers() in app.js that parses each data-on* expression
on DOMContentLoaded and attaches a proper addEventListener calling the
matching window-global function. Supports no-arg, string/number/bool/null
literals, and the `event` token.
CSP stays strict; no unsafe-inline or unsafe-hashes needed.
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>
- Move colophon (credit/email/source link) from sticky footer into
a dedicated About dialog, opened from a new header button
- Drop ~64px of bottom container padding now that the footer is gone
- Loosen vinyl-stage aspect-ratio (1:1 -> 1:0.85) so the disc no
longer leaves a tall empty band below the sleeve
- Switch tonearm height: 36% to aspect-ratio: 1 to keep proportions
consistent across the new stage ratio
- Add about.* / dialog.close i18n keys for EN and RU
- Add vinyl-variants-mockup.html as next design reference target
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.
Toggleable theater-scale player view that takes over the viewport
and amplifies the existing Studio Reference aesthetic — same fonts,
same copper/ink palette, just dialed up for immersive listening.
Layout & typography:
- Two-column centerfold: massive vinyl stage left (clamp(., 72vh, 720px)),
editorial column right with Fraunces italic title at clamp(48px, 6.4vw,
112px), Geist Mono console-style metadata strip, oversized timecodes,
full-width amplitude spectrum.
- Mobile / portrait flips to vertical theater (vinyl top, masthead+
transport below) at <=900px or any portrait orientation.
Ambient bloom:
- Duplicate of #album-art rendered behind everything at blur(110px)
saturate(1.6) opacity(0.42) — paints the room in the record's color.
Slow 28s drift animation. Light-theme variant at lower opacity.
- MutationObserver keeps bloom art in sync as tracks change.
- Vignette + edge darkening + subtle paper-grain veil frame the stage.
Interaction:
- Header button (corner-arrows-out icon) toggles; pressing 'F' anywhere
outside text inputs also toggles; ESC exits.
- Native Fullscreen API requested as best-effort sugar on top of the
CSS overlay (works on TV / tablet); CSS overlay alone covers the
CSS-only fallback case (iOS Safari, embedded webviews).
- fullscreenchange listener mirrors OS-level exit back into the overlay.
- Auto-hide chrome + cursor after 2.5s idle, restored on mousemove.
- Focus moves to play/pause on enter; restored to invoking element on
exit.
- Hides mini-player, tab bar, header, folio marks, and other tabs while
active.
Motion:
- 320ms fade-in for the stage, 600ms vinyl rise, 1.4s bloom-in,
staggered 80ms ladder for kicker -> title -> byline -> album -> meta
-> spectrum -> transport. prefers-reduced-motion disables all.
i18n:
- player.fullscreen / player.fullscreen.exit / player.fullscreen.exit_short
added to en.json and ru.json.
Files: index.html (header button + fs-chrome strip + fs-bloom layer),
styles.css (~360-line fullscreen block at end, scoped to body.is-
fullscreen-player), player.js (toggle + init + idle/key/observer
plumbing, ~170 lines), app.js (import + window export + init call).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
VU needle reflects actual audio output
- Was just a synthetic wobble bounded by the volume slider value.
Now reads RMS of the FFT bins (skipping bin 0 / DC) the visualizer
feeds in, multiplies by current volume, and applies attack/release
smoothing for analog-feeling ballistics.
- Falls back to the synthetic wobble when audio capture isn't
running so the needle still looks alive on the static fallback.
- When playback stops, needle settles to the bottom of the swing
(-45deg) instead of holding the volume position.
Spectrum width — actually fixed this time
- Root cause: CSS repeat() does NOT accept a CSS variable for its
count argument, so my `repeat(var(--spectrum-bars), 1fr)` rule
was invalid and silently dropped, leaving the legacy/auto sizing
behavior. Set grid-template-columns directly from JS to
`repeat(40, minmax(0, 1fr))`.
- CSS retains a `repeat(40, minmax(0, 1fr))` literal as a default
so the row renders sane even before JS executes.
Spectrogram canvas under vinyl
- Hidden via display: none — the editorial .spectrum row already
shows the audio spectrum; the canvas was redundant and ugly.
Element stays in DOM so the visualizer JS keeps rendering (drives
album-art bass-pulse + dynamic background bands).
Spectrum width
- grid-auto-flow: column with implicit columns wasn't reliably
stretching to fill the parent. Switch to explicit
grid-template-columns: repeat(var(--spectrum-bars), minmax(0, 1fr))
with the bar count exposed as a CSS variable from JS so the
column count and the actual bar count stay in sync.
- !important on display/grid-template-columns/width to defeat any
legacy descendant rules.
Device selection
- Picking a device in the audio-device dropdown is an explicit
signal that the user wants capture. Auto-enable the visualizer
if it isn't already on, then call applyVisualizerMode so the
WS subscription happens and the badge flips from 'Available' to
'Active'. Was only doing this when visualizer was already on,
which is why the user kept seeing 'Available, not capturing'.
Auto-enable was a no-op
- Writing 'visualizerEnabled'='true' to localStorage from app.js did
not update the exported `let visualizerEnabled` in player.js. So
applyVisualizerMode() saw the stale `false` and went into the
DISABLE branch — leaving the device 'available, not capturing'.
- Add a setVisualizerEnabled() setter exported from player.js and
call it before applyVisualizerMode() during boot.
Audio device persistence
- Save the selected device name to localStorage on change.
- On loadAudioDevices(), prefer status.current_device (server's
current state) but fall back to the localStorage value if the
server doesn't know one (e.g. after a server restart).
- If the saved device wasn't recognized by the server, push it back
via POST /api/media/visualizer/device so capture lands on it
immediately. Best-effort; no toast on failure.
- Spectrum: switch from flex to grid auto-flow so each bar gets an
equal-width column slot regardless of bar count. Fewer (40) but
fatter bars now genuinely span the full grid column.
- Spectrum height bumped to 70px, gap 4px.
- VU cluster: flex-direction: row-reverse so volume controls sit on
the LEFT, readout in the middle, VU meter on the RIGHT.
- Volume hairline divider switched from border-left to border-right
so it stays between the volume controls and the readout.
VU needle (animated)
- Synthetic wobble bounded by current volume runs only while
state==='playing'. Two combined sines + jitter make it look
like a real analog needle reacting to peaks.
- Settles back to the static volume-mapped position when paused.
Spectrum (real audio)
- Now driven by the same frequencyData the visualizer canvas
uses. Each visual bar averages a chunk of frequency bins.
- Spans are now JS-injected (60 bars) instead of hardcoded so
the bar count is no longer baked in.
- Spectrum spans full width of the masthead column, height
bumped to 56px for presence.
- CSS animation pauses (sets via `body.audio-spectrum-live`)
when JS is driving heights so the keyframes don't fight.
- Synthetic CSS animation remains as the fallback when audio
capture isn't available.
Visualizer auto-enable
- On first install with loopback support, visualizer is
enabled so the spectrum is alive out of the box.
Dynamic background
- Lower max opacity (1 → 0.45 dark, 0.35 light)
- sepia + saturate filter + hue-rotate keep it palette-aligned
with the copper editorial tones instead of fighting them
- mix-blend-mode screen (dark) / multiply (light) blends into
the page background instead of overlaying
Update + connection banners
- Fully restyled: glassy card with copper hairline accent,
mono uppercase text, copper hairline-border CTA buttons,
minimal close button. Matches the rest of the editorial
palette instead of the old solid-green-bar look.
Side-by-side comparison surfaced several layout regressions vs. the
mockup. This commit lands all of them at once.
Header
- Restore centered "Media Server / Studio Reference Edition" wordmark
in italic Fraunces
- Move folio marks to fixed page corners (visible on every tab):
TL = green pulse + "Connected · Local 8765"
TR = "Vol. I — Studio Reference · v0.x.x"
- Replace boxed version-label badge with copper mono inline in folio.tr
- Reduce header-to-content gap (container padding-top 28→56 with the
folio now anchored above)
Player view
- Spectrum bars: smaller height (32px), centered with max-width so
they don't span the whole right column
- Spectrogram canvas: hidden by default (opacity 0); reveals only when
visualizer toggle is active. No more leaking into bottom-left.
- VU cluster volume controls: strip legacy box (background, padding,
border-radius); compact stacked layout with thin slider, small mute
button, mono "VOL · XX%" readout
- Disable legacy applyVinylMode() — the .vinyl class added a SECOND
rotation animation on top of the structural .vinyl-stage spin,
causing visible compounding. Vinyl is now purely structural.
Toggles
- Remove vinyl mode toggle button (vinyl is always on)
- Keep audio visualizer (spectrum vis) toggle — still shown by JS
when supported
Mini player
- Force always-visible on non-player tabs regardless of scroll, by
short-circuiting setMiniPlayerVisible when activeTab !== 'player'
i18n
- New keys: header.connected, header.volume, header.edition,
header.edition_sub
- Removed unused: player.folio_left, player.folio_right
- en.json + ru.json updated
Patches HTMLDialogElement.prototype.showModal globally to move focus
onto the dialog element itself instead of the first focusable
descendant. On touch devices the previous behavior popped up the
on-screen keyboard whenever a modal opened, which was confusing.
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>
- 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>
- Registry of 17 popular media apps (browsers, players, streaming)
- Substring matching resolves raw process names to friendly names
- Brand-colored SVG icons displayed inline next to source name
- Russian locale support for Yandex Music (Яндекс Музыка)
- Unknown sources fall back to .exe-stripped name
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Native select with explicit font stack and focus glow
- Hide mini player volume section below 900px
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Save vinyl rotation angle before flipping vinylMode flag off
- Wrap vinyl + visualizer buttons in .player-toggles container
- Move margin-left:auto from individual buttons to group
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>
- Save current rotation angle to localStorage every 2s and on page unload
- Restore angle on page load via CSS custom property --vinyl-offset
- Extract angle from computed transform matrix
Co-Authored-By: Claude Sonnet 4.5 <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>
- 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>
- Vinyl mode: album art as center label with grooves, spindle hole, vignette
- Smooth transition between normal and vinyl modes
- Accent color picker with 9 preset colors in header
- Fix progress bar hover layout shift (use scaleY instead of height)
- Fix glow position jump when toggling vinyl mode
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Compact header and footer (reduced padding, margins, font sizes)
- Remove separator borders from header and footer
- Fix color contrast on hover states (white text on accent backgrounds)
- Fix album art not updating on track change (composite cache key)
- Slow vinyl spin animation (4s → 12s)
- Replace Add buttons with dashed-border add-cards
- Fix dialog header text color
- Make theme toggle button transparent
Co-Authored-By: Claude Sonnet 4.5 <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>
- 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>
- Search bar appears when browsing inside a folder
- Client-side filtering with 200ms debounce
- Clear button and search icon
- Hides at root level, resets on navigation
- Localized placeholder (en/ru)
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 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 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>