# MBXHub API > MusicBee REST API - v0.5.2.4 > > llms.txt specification: https://llmstxt.org/ > Requires MusicBee 3.1+ (API rev 53). Supports up to API rev 58 (MusicBee 3.6+ APIs available when present). ## Endpoints - Base URL: `http://localhost:8080` - WebSocket: `ws://localhost:8080/ws` - API Docs: `http://localhost:8080/docs` ## Player Control - `POST /player/play` - Start playback - `POST /player/pause` - Pause playback - `POST /player/playpause` - Toggle play/pause (alias: `/player/play-pause`) - `POST /player/stop` - Stop playback - `POST /player/next` - Next track - `POST /player/previous` - Previous track - `GET /player/volume` - Get volume (0-100) - `PUT /player/volume` - Set volume (body: {"volume":50}) - `GET /player/shuffle` - Get shuffle state - `PUT /player/shuffle` - Set shuffle (body: {"shuffle":true/false}) - `GET /player/autodj` - AutoDJ state - `POST /player/autodj/start` - Start AutoDJ - `POST /player/autodj/stop` - Stop AutoDJ - `GET /player/repeat` - Get repeat mode - `PUT /player/repeat` - Set repeat (off/all/one) - `GET /player/status` - Full player state - `GET /player/mute` - Mute state - `PUT /player/mute` - Set mute (body: {"mute":true}) - `GET /player/position` - Playback position in ms - `PUT /player/position` - Seek (body: {"position":ms}) - `POST /player/next-album` - Skip to next album - `POST /player/previous-album` - Skip to previous album - `POST /player/queue-random` - Queue a random track (alias: `/player/queuerandom`) ### Audio Processing - `GET /player/equaliser` - Get equalizer state (also `/player/equalizer`) - `PUT /player/equaliser` - Set equalizer - `GET /player/dsp` - Get DSP enabled state - `PUT /player/dsp` - Set DSP enabled - `GET /player/crossfade` - Get crossfade state - `PUT /player/crossfade` - Set crossfade - `GET /player/replaygain` - Get replay gain mode - `PUT /player/replaygain` - Set replay gain mode - `GET /player/scrobble` - Get scrobble (Last.fm) state - `PUT /player/scrobble` - Set scrobble ### Display Settings - `GET /player/stopaftercurrent` - Get stop-after-current state - `POST /player/stopaftercurrent` - Toggle stop-after-current - `GET /player/show-time-remaining` - Show time remaining preference - `GET /player/show-rating-track` - Show rating on track preference - `GET /player/show-rating-love` - Show love rating preference - `POST /player/show-equaliser` - Open the equaliser window in MusicBee (also `/player/show-equalizer`) - `GET /player/button-enabled?button=N` - Check if a UI button is enabled - `GET /player/output-devices` - List audio output devices - `PUT|POST /player/output-device` - Switch output device (body: {"device":"name"}) ## Device & Mixer Control ### Windows Audio Device - `GET /devices/audio/outputs` - List all active Windows audio render devices (name, id, isDefault) - `GET /devices/audio/volume` - Windows audio device volume and mute state - `PUT /devices/audio/volume` - Set Windows audio device volume (body: {"volume":50}) - `PUT /devices/audio/mute` - Set Windows audio device mute (body: {"mute":true}) ### Network Endpoints - `GET /devices/endpoints` - List configured network endpoints - `POST /devices/endpoints` - Add a network endpoint (body: {"ip":"192.168.1.50","type":"devialet","name":"Living Room"}) - `POST /devices/endpoints/scan` - Scan local network for Devialet speakers via mDNS (4s browse, returns discovered devices with name, ip, alreadyConfigured flag) - `DELETE /devices/endpoint/{id}` - Remove a saved endpoint - `GET /devices/endpoint/{id}/volume` - Get endpoint volume and mute state - `PUT /devices/endpoint/{id}/volume` - Set endpoint volume (body: {"volume":50}) - `PUT /devices/endpoint/{id}/mute` - Set endpoint mute (body: {"mute":true}) - `GET /devices/endpoint/{id}/sources` - List endpoint input sources - `PUT /devices/endpoint/{id}/source` - Select endpoint input source (body: {"sourceId":"upnp"}) ### Mixer Settings - `GET /mixer/settings` - Get mixer settings (default fader) - `PUT /mixer/settings` - Set mixer settings (body: {"defaultFader":"device"}) - `GET /mixer/volume` - Get volume from default fader (player or device) - `PUT /mixer/volume` - Set volume on default fader (body: {"volume":50}) ### Streaming - `POST /player/open-stream` - Open a stream handle for playback - `POST /player/update-play-statistics` - Update play count and statistics ## Now Playing - `GET /nowplaying` - Current track metadata - `GET /nowplaying/artwork` - Album art (binary image) - `GET /nowplaying/lyrics` - Track lyrics - `GET /nowplaying/position` - Playback position in ms - `GET /nowplaying/property?type=X` - File property (Size, Bitrate, DateAdded, etc.) - `GET /nowplaying/tag?field=X` - File tag (Artist, Album, Genre, Rating, etc.) - `GET /nowplaying/artwork-url` - Artwork as data URL (base64) - `GET /nowplaying/downloaded-artwork` - Downloaded artwork (binary) - `GET /nowplaying/downloaded-artwork-url` - Downloaded artwork as data URL - `GET /nowplaying/downloaded-lyrics` - Downloaded lyrics from provider - `GET /nowplaying/artist-picture` - Artist picture (binary, ?fadingPercent=0) - `GET /nowplaying/artist-thumbnail` - Artist thumbnail (smaller) - `GET /nowplaying/artist-picture-urls` - Artist picture URLs (?localOnly=false). Response includes a `pictures` array with `src` URLs (serveable via `/nowplaying/artist-pictures/{index}`) in addition to raw file paths. - `GET /nowplaying/artist-pictures/{index}` - Serve current artist's Nth picture as binary image (0-based). Query: ?localOnly=true - `GET /nowplaying/is-soundtrack` - Whether current track is a soundtrack - `GET /nowplaying/soundtrack-pictures` - Soundtrack picture URLs - `GET /nowplaying/spectrum` - Real-time spectrum data (frequency analysis) - `GET /nowplaying/sound-graph` - Waveform graph data - `GET /nowplaying/sound-graph-ex` - Extended waveform graph data - `GET /nowplaying/peak` - Current stereo peak and RMS levels (0.0-1.0). Returns {peak: [L, R], rms: [L, R]}. Requires MusicBee 3.6+ (API rev 58+) ## Queue - `GET /queue` - List queue (?offset=0&limit=50) - `GET /queue/current` - Current track index - `POST /queue/add` - Add tracks (body: {"url":"path","position":"next|last"} or batch: {"urls":["path1","path2"],"position":"last"}) - `POST /queue/playnow` - Play track immediately (body: {"url":"path","cueTrack":3}) - `POST /queue/clear` - Clear queue - `POST /queue/move` - Reorder (body: {"from":0,"to":5}) - `DELETE /queue/{index}` - Remove track by index - `POST /queue/play` - Play track at index (body: {"index":3,"cueTrack":1}) - `GET /queue/next-index?offset=1` - Get index N tracks ahead/behind - `GET /queue/has-prior` - Whether queue has prior tracks - `GET /queue/has-following` - Whether queue has following tracks - `POST /queue/play-library-shuffled` - Clear queue and play library shuffled - `GET /queue/{index}/url` - Get file URL for track at index - `GET /queue/{index}/tag?field=X` - Get tag for track at index - `GET /queue/{index}/property?type=X` - Get property for track at index ### Virtual Tracks (CUE) Virtual Tracks share the same audio file URL. The queue response includes Virtual Track metadata: - `cueTrack` - Track number when detected - `cueStartMs` - Start offset in milliseconds within the physical file - Track titles from CUE sheet (not embedded audio metadata) - Include `cueTrack` in add/playnow to preserve track identity ### CUE Track Resolution MBXHub detects CUE-backed audio files and resolves per-track metadata automatically across all surfaces: - `GET /player/status` - Returns CUE track title, artist, trackNo, cueTrack field - `GET /nowplaying/position` - Includes `cueTrack` and `cueTitle` when active - `GET /nowplaying/tag?field=TrackTitle` - Returns CUE track title (not base file) - WebSocket `TrackChanged` events include `cueTrack` field when CUE active - Dashboard shows resolved CUE track metadata - Listen Here streaming: seeks to `cueStartMs`, shows track-relative progress, auto-advances at track end CUE parsing uses encoding detection (BOM, UTF-8 validation, Windows-1252 fallback). Optional: drop CueSharp.dll in Plugins folder for enhanced parsing (loaded via reflection). ## Library - `GET /library/files` - Query files (?query=&artist=&albumArtist=&album=&genre=&offset=&limit=&sort=) - `GET /library/artists` - List artists (?offset=&limit=&sort=) - `GET /library/albums` - List albums (?artist=&offset=&limit=&sort=) - `GET /library/albums/detailed` - Albums with firstTrackUrl, year, dateAdded for artwork lookups and sorting (?offset=&limit=) — eliminates per-album /library/files round-trips - `GET /library/album-artists` - List album artists (?offset=&limit=) - `GET /library/albums/by-artist` - Albums for an artist (?albumArtist= or ?artist=, &sort=alpha|year|year-asc). ?artist= queries ArtistPeople (broader), ?albumArtist= queries AlbumArtist (exact credit) - `GET /library/albums/unheard` - Albums where all tracks have playCount=0 (?offset=&limit=) - `GET /library/albums/with-pdf` - Albums that contain PDF booklets (?offset=&limit=) - `GET /library/genres` - List genres (?sort=) - `GET /library/inbox` - Files in MusicBee Inbox (Source Type 4). Progressive: browse tab hidden if empty - `GET /library/audiobooks` - Audiobook files (Source Type 32). Progressive: browse tab hidden if empty - `GET /library/videos` - Video files (Source Type 64). Progressive: browse tab hidden if empty - `GET /library/search?q=term` - Full-text search over title/artist/album/genre. Two modes: **strict** (default) requires each query word to be a prefix of a real word ("St Anger" does NOT match "Stranger"); **substring** is loose ("anger" matches "Stranger"). Default mode from `Library.DisableSubstringSearch` setting (true=strict, shipped default). Override per-call with `?substring=true|false`. In strict mode, multi-word queries additionally require adjacent-token matching within a single field (title, artist, album, or genre) — controlled by `Library.DisableCrossFieldMatching` (true=adjacent required, shipped default; set to false for legacy cross-field behavior). Diacritic and punctuation normalized. Response includes `mode: "strict"|"substring"`. Also supports `?sort=`. - `GET /library/file/{url}` - Get file metadata - `PUT /library/file/{url}` - Update tags (body: {"Artist":"name","Title":"name"}) - `GET /library/files/raw` - Raw file path list (no metadata, faster) - `GET /library/cuetest?query=` - Test CUE track resolution for a query - `POST /library/add` - Add file to library (body: {"url":"path"}) - `POST /library/artwork/batch` - Batch artwork fetch (body: {"urls":["path1","path2",...]}, max 50). Returns base64 data URIs keyed by URL, null for missing artwork - `POST /library/find-device-ids` - Find device persistent IDs (body: {"urls":[...]}) - `POST /library/sync-delta` - Get sync delta (body: {"deviceId":"...","since":"ISO date"}) - `POST /library/commit` - Commit pending tag changes to file (body: {"file":"path"}). Use after batching Library_SetFileTag RPC calls - `GET /library/evaluate?expression=&fileUrl=` - Evaluate MusicBee expression (template syntax: , $If(), virtual tags). fileUrl optional, defaults to now-playing. Requires MusicBee 3.4.1+ (API rev 55+) - `GET /library/no-artwork` - MusicBee's placeholder image for tracks with no artwork (binary). Cache-Control: 24h. Requires MusicBee 3.5+ (API rev 57+) - `GET /library/file/{url}/lyrics` - Lyrics for specific file - `GET /library/file/{url}/artwork` - Artwork for specific file (binary) - `GET /library/file/{url}/artwork-url` - Artwork as data URL for specific file - `GET /library/file/{url}/artwork-count` - Returns `0` (no artwork) or `1` (artwork present). Probes up to 20 embedded-artwork LOCATIONS and picks the largest byte-size as the canonical image; `/artwork?index=0` then serves that variant. Cached per fileUrl. - `GET /library/file/{url}/pdf` - PDF booklet from track's album folder (binary, application/pdf) - `GET /library/file/{url}/fan-art` - List all images in the track's album folder and one level of subfolders. Returns {folder, images:[], count}. Excludes: canonical primary-cover filenames (folder/cover/front/album × .jpg/.jpeg/.png) — duplicates of the primary artwork served by `/artwork`; ALL Windows Media Player cache files (AlbumArtSmall*, AlbumArt_*); thumbnails (<5KB); Thumbs.db; desktop.ini. Security: track must be in MusicBee library. - `GET /library/fan-art/{path}` - Serve a single fan art image by absolute path (binary). Security: image must be in a directory containing a MusicBee library file. Cache-Control: 1 hour. - `GET /library/file/{url}/has-pdf` - Check if PDF booklet exists (returns {hasPdf: bool, url: string}) - `GET /library/file/{url}/device-id` - Device persistent ID - `PUT /library/file/{url}/device-id` - Set device persistent ID - `GET /library/artist/{name}/similar` - Similar artists (?limit=10) - `GET /library/artist/{name}/picture` - Artist picture (binary) - `GET /library/artist/{name}/thumbnail` - Artist thumbnail (smaller) - `GET /library/artist/{name}/pictures` - Artist picture URLs (?localOnly=false). Response includes a `pictures` array with `src` URLs (serveable via `/library/artist/{name}/pictures/{index}`) in addition to raw file paths. - `GET /library/artist/{name}/pictures/{index}` - Serve artist picture by index as binary image (0-based, from /pictures list). Query: ?localOnly=true - `GET /library/recent` - Recently played tracks sorted by last played descending (?limit=50&offset=0&days=30) - `GET /library/videos` - Video files from MusicBee's Video library node. Returns `{total, videos: [{url, title, artist, album, kind, duration}]}`. Title falls back to filename when tag is empty. Uses Source Type 64 query ## Radio - `GET /radio/stations` - List radio stations (returns `{total, stations: [{url, name}]}`) ## Playlists - `GET /playlists` - List all playlists - `POST /playlists` - Create playlist (body: {"name":"My Playlist","files":["path1","path2"]}) - `GET /playlists/{url}/files` - Get playlist tracks - `POST /playlists/{url}/files` - Add tracks (body: {"urls":["path1","path2"]}) - `POST /playlists/{url}/play` - Play playlist - `GET /playlists/{url}` - Get playlist details - `PUT /playlists/{url}` - Update playlist name - `DELETE /playlists/{url}` - Delete playlist ## WebSocket Connect to `ws://localhost:8080/ws` for real-time events: ```javascript const ws = new WebSocket('ws://localhost:8080/ws'); ws.onmessage = (e) => { const msg = JSON.parse(e.data); // msg.event: TrackChanged, PlayStateChanged, VolumeChanged, etc. // msg.data: event-specific payload // msg.timestamp: ISO 8601 timestamp }; // Subscribe to specific events: ws.send(JSON.stringify({ subscribe: ['TrackChanged', 'PlayStateChanged'] })); ``` Event types: - `TrackChanged` - {"fileUrl":"...","title":"...","artist":"...","album":"...","duration":ms,"artworkUrl":"/nowplaying/artwork"} - `PlayStateChanged` - {"state":"playing|paused|stopped"} - `PositionChanged` - {"position":ms,"duration":ms} - `VolumeChanged` - {"volume":0-100,"muted":bool} - `ShuffleChanged` - {"enabled":bool} - `RepeatChanged` - {"mode":"none|all|one"} - `QueueChanged` - {"action":"changed","index":-1,"totalTracks":-1} - `MetadataChanged` - {"fileUrl":"...","rating":-1 to 5,"love":"L|B|"} - `Reaction` - {"emoji":"fire","type":"fire","nickname":"Guest","trackTitle":"...","trackArtist":"..."} - `TasteChanged` - {"topGenres":[...],"topArtists":[...],"bpmRange":[lo,hi],"mood":"...","moodConfidence":0.0-1.0,"influenceCount":N,"reactionCount":N} - `ThemeChanged` - {"activeMode":1|2,"accentHue":0-360,"bgHue":0-360,"bgLightness":0-100,"textLightness":0-100} - `MoodCacheWarmed` - {"librarySize":N,"fromLocalFile":N,"fromMetaServer":N,"stillMissing":N,"durationMs":N} — fires after MoodCache warm-up finishes (both startup and the 15-min delta refresh tick). Mirrors the `lastWarmup` fields from `GET /autoq/mood-cache/status`. Clients can refresh mood-cache UI on this event instead of polling. ## Static Pages (/pages/) MBXHub serves HTML, CSS, and JS from the `/pages/` directory. ### How It Works - Files location: `%APPDATA%\MusicBee\MBXHub\pages\` - Access via: `http://{host}/pages/yourfile.html` - Supports: .html, .css, .js, .json, images - No build step - just save and refresh ### Included Pages Read these for working reference code: - `GET /pages/index.html` - Landing page listing available views - `GET /pages/player.html` - Full 3-column player (browse iframe, now playing, queue) with drag-drop queue reorder and touch support. Browse panel embeds browse.html via iframe with theme sync via postMessage - `GET /pages/nowplaying.html` - Focused now-playing view with artwork, lyrics, and queue. Drag-drop queue reorder on upcoming tracks. Auto-stacks below 800px with accordion sections (Now Playing, Queue, Lyrics) - `GET /pages/browse.html` - Library browser: 8 tabs (Albums/Artists/Genres/Playlists/Tracks/Podcasts/Radio/Moods) with responsive tab sizing, drilldown and breadcrumbs, diacritic-normalized fuzzy search with 8-category grouped results, album art grid with lazy loading, batch queue via long-press, playlist picker, search-driven Tracks tab via /library/search, video items with direct play (no drilldown), auto-search fallback (when inline name filtering yields no results, auto-triggers full-text server search), light/dark theme - `GET /pages/config.html` - Settings and configuration interface for dashboard and AutoQ - `GET /pages/autoq.html` - AutoQ Tuning Console: mixer-style sliders for all scoring weights, reaction scores, and normalization ranges - `GET /pages/mixer.html` - Unified fader mixing surface: three independent faders for Player (MusicBee), Device (Windows audio), and Endpoint (network speaker) volume. Configurable default fader, mute controls, endpoint source selection. Charm bar integration via postMessage - `GET /pages/explore.html` - Album art explorer: browse albums as artwork with source filters (All/1mo/3mo/12mo/Unheard), search, sort, image gallery with embedded artwork probing, PDF booklet viewing, play/queue controls, theme sync. Accepts `?album=&artist=` query params for seeding from search or dashboard. Artist discography grid in expanded view shows all albums by the current artist. Expanded-view hero has paired close (×) and dashboard home (⌂) buttons — close dismisses the overlay and stays on explore; home navigates to `/dashboard`. Home is hidden in iframe-mode - `GET /pages/media` - Media: full-bleed image/video viewer from configured folders with auto-rotation, crossfade, category browsing, keyboard/touch navigation ### Shared Resources - `GET /pages/queue-dragdrop.js` - Shared drag-and-drop queue reorder module. Mouse drag (event delegation) + touch long-press (300ms) with visual clone and auto-scroll. Used by player.html, nowplaying.html, and partymode/dj.html ### Creating New Pages To build a custom UI: 1. Create an HTML file 2. Save to `%APPDATA%\MusicBee\MBXHub\pages\` 3. Access at `http://{host}/pages/yourfile.html` 4. Use fetch() for REST API, WebSocket for live updates 5. Reference `/pages/player.html` for working patterns ### Minimal Starter ```html My Player
Loading...
``` Save as `myplayer.html` in pages folder, access at `/pages/myplayer.html` ### Best Practices Follow these patterns for robust, performant, accessible custom pages. #### CSS Foundation All MBXHub pages use these CSS best practices: ```css :root { color-scheme: dark; /* Or dark light for theme switching */ /* ... CSS variables ... */ } /* Respect user motion preferences */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } /* Box-sizing reset */ *, *::before, *::after { box-sizing: border-box; } /* Focus styles for keyboard navigation */ button:focus-visible, input:focus-visible, a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } /* Cross-browser scrollbar styling */ .scrollable { scrollbar-width: thin; /* Firefox */ scrollbar-color: rgba(255,255,255,0.1) transparent; } .scrollable::-webkit-scrollbar { width: 6px; } /* WebKit */ .scrollable::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; } ``` ## ARiA (Automation) - `GET /aria/status` - Check if ARiA is enabled - `POST /aria/send-keys` - Send keyboard input (body: {"keys":"^a"}). Prefix keys with `!` to send to current foreground window without refocusing MusicBee - `GET /aria/wake` - Wake PC from sleep (also POST) - `GET /aria/presets` - List available presets - `GET /aria/programs` - List allowed programs for run() command - `GET /aria/preset/{name}` - Execute preset (also POST) - `POST /aria/focus` - Focus MusicBee window - `POST /aria/mouse/move` - Move mouse (body: {"x":100,"y":200}) - `POST /aria/mouse/click` - Click mouse (body: {"button":"left","x":100,"y":200}) ### ARiA Script Commands Presets use semicolon-separated script commands: - `sndkeys(keys)` / `sendkeys(keys)` - Send keyboard input (SendKeys or DuckyScript format). Prefix `!` to skip MusicBee refocus: `sendkeys(!{ENTER})` sends to current foreground window - `delay(ms)` / `wait(ms)` - Pause execution (max 30000ms) - `run(name[,extraArgs])` / `exec(...)` - Launch a pre-configured program from `ariaAllowedPrograms` in settings. Programs must be defined in mbxhub.json to be executable - `webhook(url,method,body)` / `http(...)` - HTTP request. Prefix `!` on URL for fire-and-forget - `click(x,y,button)` / `mouseclick(...)` - Mouse click at coordinates - `volume(action)` / `vol(action)` - Volume: up, down, mute, +N, -N - `toast(msg)` or `toast(title,msg)` - Windows balloon notification - `restart(target)` - Restart: mb/musicbee/app, system/host, shutdown ### ARiA Settings Key settings in `mbxhub.json`: - `ariaEnabled` (default: false) - Enable ARiA automation endpoints - `ariaWebhookTimeoutMs` (default: 10000) - Timeout for webhook commands in milliseconds - `ariaPresets` - Named scripts for quick execution (default: RIA1-9 presets mapped to Ctrl+Alt + home row keys) - `ariaCommands` - Custom command aliases and macros extending the built-in command set - `ariaAllowedPrograms` - Allowlist for the run() command. Each entry has: name (lookup key), path (executable), args (default arguments, optional), hidden (no console window, optional). Empty by default — run() does nothing until programs are configured - `allowRemoteExit` (default: false) - Allow remote shutdown of MusicBee via ARiA ## RemoteApp - `GET /remoteapp/status` - RemoteApp status (configured, supported, rdpEnabled, edition, enabled, apiEnabled) - `GET /remoteapp/rdp` - Download .rdp file for MusicBee RemoteApp connection (blocked when remoteAppApiDisabled) - Query: `?hostname=192.168.1.100` (optional, defaults to request Host header) - Any other query param forwarded as .rdp setting override (e.g. `?audioqualitymode=0&redirectprinters=1`) - Client (Pro/Enterprise): uses full exe path from registry. Server: uses `||AppName` alias for RDS lookup. ### RemoteApp Settings Key settings in `mbxhub.json`: - `remoteAppEnabled` (default: false) - Enable RemoteApp feature - `remoteAppApiDisabled` (default: false) - Disable /remoteapp/rdp endpoint (API is on by default when feature is enabled) - Dashboard footer links configurable via `dashboardFooterLinks` in settings (null = defaults) ### Prerequisites - Windows Client (Pro/Enterprise): (1) Settings > System > Remote Desktop > ON. (2) Allow through firewall (auto-prompted, or manually via Control Panel > Firewall > Allow an app > Remote Desktop). (3) NLA is on by default; disable if older clients cannot connect. - Windows Server: Install RDS role (`Install-WindowsFeature RDS-Connection-Broker, RDS-Web-Access, RDS-RD-Server -IncludeManagementTools`) ### RemoteApp CLI - `MBXHub.exe remoteapp setup --path ` - Configure RemoteApp (requires elevation, Pro/Enterprise/Server) - `MBXHub.exe remoteapp setup --detect` - Auto-detect MusicBee and configure - `MBXHub.exe remoteapp remove` - Remove RemoteApp configuration (requires elevation) - `MBXHub.exe remoteapp status` - Show current status - `MBXHub.exe remoteapp rdp --hostname ` - Generate .rdp file content ## Media Serves images and videos from configured root directories. Categories are subfolders under each root. No MusicBee library integration — purely file-serving from user-configured folders. ### Settings (mbxhub.json) - `media.imageRoot` (default: "") - Root folder for images. Subfolders become categories. Empty = disabled. - `media.videoRoot` (default: "") - Root folder for videos. Subfolders become categories. Empty = disabled. - `media.intervalSeconds` (default: 30, range: 5–300) - Rotation timer in seconds. - `media.shuffle` (default: true) - Random order (true) or alphabetical sequential (false). ### Endpoints - `GET /media/settings` - Returns `{intervalSeconds, shuffle}` for page/charm consumption. - `GET /media/images/categories` - List subfolder names under imageRoot. Files in root listed as `_root`. - `GET /media/videos/categories` - List subfolder names under videoRoot. - `GET /media/images/{category}` - List filenames in category. Returns `{name, files:[], count}`. Filenames only (no paths). - `GET /media/videos/{category}` - List filenames in category. - `GET /media/images/{category}/next` - Serve next image (rotation state, sequential or shuffled). - `GET /media/videos/{category}/next` - Serve next video (rotation state, HTTP Range support). - `GET /media/images/{category}/file/{name}` - Serve specific image by filename. - `GET /media/videos/{category}/file/{name}` - Serve specific video by filename (HTTP Range support for seeking). ### Supported Formats - Images: .jpg, .jpeg, .png, .bmp, .webp, .gif - Videos: .mp4, .webm, .mkv, .avi, .mov, .asf, .wmv (.mp4/.webm inline, others open externally) - Minimum file size: 5KB (skips thumbnails) ### Security - All paths validated under configured root — no directory traversal. - Category names mapped to actual subfolders only. - Filenames validated against actual directory contents. - Unconfigured/empty root returns empty array for categories, 404 for file requests. ### Page - `GET /pages/media` - Full-bleed media viewer with auto-rotation, crossfade transitions (images), video playback with seek support, chrome overlay with category/mode selectors, keyboard/touch/swipe navigation. ## Audio Streaming - `GET /stream/{path}` - Stream an audio or video file from the MusicBee library - Path: URL-encoded absolute file path (e.g. `/stream/C%3A%5CMusic%5Csong.mp3`) - Supports HTTP Range requests for seeking (206 Partial Content) - MIME types: mp3=audio/mpeg, flac=audio/flac, m4a/mp4=audio/mp4, ogg/oga=audio/ogg, wav=audio/wav, opus=audio/opus, aac=audio/aac, wma=audio/x-ms-wma, aiff/aif=audio/aiff - Security: path traversal blocked, audio/video extensions only, must be in MusicBee library - 400=invalid path, 403=not in library, 404=not found or streaming disabled, 416=invalid range - CUE tracks: client uses `cueStartMs` from track data to seek to correct offset; progress and boundaries handled client-side - Disable: `disableStreaming` setting (returns 404 when true) - Feature flag: `streaming` in `GET /system/features` response ## Device Proxy - `POST /api/proxy` - Forward HTTP request to a LAN device (CORS bypass) - Body: `{"method":"GET|POST|PUT", "url":"http://192.168.x.x/...", "body":{} }` - Only private IPs allowed (RFC 1918 + loopback). Public internet blocked (403). - Response: target device response passed through verbatim (status code + body). - 502 if device unreachable. 5-second timeout. - Disable: `apiDisableProxy` setting (returns 404 when true). ## Charms Charms are configurable action buttons on the dashboard charm bar. Each charm is a `.json` manifest in the `charms/` folder (MBXHub data directory). MBXHub seeds built-in charms on first run: `mixer.json` (volume mixer), `browse.json` (Library Browser), and `aria.json` (ARiA automation — uses the new action-menu display mode). Hidden in party mode. ### Manifest fields - `id` - Unique identifier - `icon` - Emoji or character for the button - `label` - Tooltip / display name - `action` - Action type: - `webapp /path` - Open HTML page (standalone tab or inline iframe) - `iframe-cmd ` - Send postMessage to charm iframe (auto-loads if needed) - `http://...` - Fire HTTP request (cross-origin routed via `/api/proxy`) - `display` - `standalone` (new tab), `inline` (iframe), `both` (click=inline, shift+click=standalone), or `action-menu` (single trigger + popover of `expand[]` items — used by the built-in ARiA charm) - `msg` - Status message after execution - `context` - Optional: `library-only` or `stream-only` - `expand` - Array of sub-actions (icon, label, action, display, msg). Renders as grouped button row with connection status dot. ### Settings (`charmBar` in mbxhub.json) - `order` (default: []) - Charm IDs in display order. New charms appended automatically. - `hidden` (default: ["endpoints","explore","projector"]) - Charm IDs to hide from the dashboard. - `buttonSize` (default: "M") - Bar-level button size preset: S (36px), M (44px), L (52px), XL (64px), XXL (76px). S/M were bumped in v0.5.2.1 (was 32/40); XL/XXL added in v0.5.2.3 for high-density touch displays (11.6" FHD @ 150% DPI needs ~76px to hit 0.6" physical). - `sizeOverrides` (default: {}) - Per-charm size map (charm ID → S/M/L/XL/XXL). Individual charms can break from the bar-level size via this override. - `breakBefore` (default: []) - Charm IDs that force a new row in the charm bar. Combined with `sizeOverrides`, one charm (e.g. the mixer) can occupy an XL row while the rest stay compact. - `displayOverrides` (default: {}) - Per-charm display mode (charm ID → "inline" or "standalone"). Overrides the charm manifest's default. The charm bar is also a dashboard layout section (`charms`) that can be reordered/hidden via `dashboardLayout`. ## System - `GET /ping` - Health check (alias: `GET /system/ping`) - `GET /status` - HTML status page with live dashboard: system info (version, uptime, host, modules), library stats (tracks, albums, artists, genres, playlists, podcasts), AutoQ state (status, vibe list, mood cache, auto scan), and feature flags - `GET /system/status` - Server status - `GET /system/version` - Version info (includes `host` field with discovery name or machine name) - `GET /system/capabilities` - Returns this node's capabilities for cross-channel SSDP discovery. Response: `{ node, version, capabilities[], endpoints{} }`. Used by Shell and other MBXHub nodes to discover what this instance offers. - `GET /system/fleet` - Aggregator across the local Shell plus every peer from `/meta/sync/status`. Parallel fetch of each node's `/meta/health` + `/meta/stats` with 3s per-node timeout. Response: `{ success: true, data: { fetchedAt, nodeCount, nodes[] } }`. Each node entry has `{ name, url, status (up|down|timeout|unknown), tracks, withFeatures, localScanned, peerSynced, queueDepth, batchSize, scanMode (auto|local|off), scanStatus, truedatFound, syncEnabled, syncing, lastSyncAt, lastSyncResult, version, uptime, dbSizeMb }`. Down / timeout nodes only carry name, url, status, error. Single-pane-of-glass for distributed scanning: `batchSize` + `queueDepth` + `scanMode` together tell you which peer is actively scanning and at what parallelism. - `GET /system/topology` - **Layer 3 fleet topology.** Returns `{success, localNode, discovered[], preferences, fetchedAt}`. `discovered[]` is local + KnownPeers; each node has `{name, url, capabilities[], healthy, reachable}` derived from the node's `/meta/capabilities` + `/meta/health`. `preferences` echoes the current `TopologyPreferences` block: `metaServerOrder[]`, `scanDispatchOrder[]` (accepts `"local"`), `scanExcludes[]`, `peerPullFanOut[]`. All empty = zero behavior change. Access: Shuffle category. - `PUT /system/topology/preferences` - Partial update of TopologyPreferences. Absent fields keep current value. Empty body rejected — to reset a list, send explicit empty array (`{"metaServerOrder":[]}`). Each list ≤ 32 entries, entries must parse as URIs (except `"local"` in `scanDispatchOrder`). Honor points: `metaServerOrder` → `HubController.ResolveMetaServer` preflight; `scanDispatchOrder` → `ScannerPool.Dispatch` ordering; `scanExcludes` → dispatch filter (wins over order); `peerPullFanOut` → `PeerPullService` peer order. Follows Settings Safety Rule 3 — reload-before-save runs atomically inside `HubSettings.Mutate()` under the settings lock. Errors: `400 BAD_REQUEST` (empty body / invalid URI / `"local"` outside scanDispatchOrder / list > 32 / non-string element), `400 INVALID_JSON` (unparseable body), `503 SETTINGS_LOCKED` (settings path not configured — startup wiring issue), `500 SAVE_FAILED` (Mutate threw or returned false). Non-PUT methods on this path return 404 from the router — the defensive 405 check inside the handler is unreachable. - `GET /system/fleet/queue?node=NAME&limit=N` - Per-node queue snapshot, proxies to the resolved Shell's `/meta/queue`. Same-origin pass-through avoids cross-origin blocks in a multi-host fleet. `limit` default 20, max 100. - `GET /system/fleet/activity?node=NAME&since=SEQ&limit=N` - Per-node scan activity feed, proxies to Shell's `/meta/activity`. Incremental via `since`; first poll with `since=0` seeds the cursor and returns the last N events. `limit` default 50, max 200. Each event carries `dispatchedTo` (executing ScanWorker's `Environment.MachineName`) so aggregated feeds show which node actually ran each scan — useful for visualizing dispatch flow when one peer routes work to another. - `GET /system/features` - Enabled feature flags. Response fields: `banlist`, `ratings`, `loved`, `reactions`, `streaming`, `autoq` - `GET /system/metrics` - Performance metrics - `GET /system/uptime` - Server start time and uptime duration - `GET /system/client-ip` - Returns client's IP as seen by the server. Used by the SMTC Link Charm to discover the client's local Shell on remote dashboards. - `GET /system/settings` - Get MBXHub settings. Response includes `webSocketPort` (legacy — always equals `restPort`) - `PUT /system/settings` - Update settings (also POST). Security: `restPort` and `restEnabled` changes require localhost (403 for remote). All changes blocked during party mode for remote clients. - `GET /system/settings/schema` - Schema for all configurable settings (type, range, default, current value). Blocked during party mode and when remote config is disabled. - `PUT /system/config` - Update configurable settings via dotted keys. Only [ConfigSetting]-scoped properties. Blocked by read-only mode, party mode, remote config disabled. - `POST /system/config` - Alias for `PUT /system/config` - `GET /system/default-page` - Get default redirect page - `PUT /system/default-page` - Set default page (body: `{"defaultPage":"/pages/player.html"}`) - `GET /system/theme` - Returns current theme configuration (activeMode, mode1, mode2 HSL values, active computed mode) - `PUT /system/theme` - Update theme configuration. Body: `{"activeMode":1|2, "mode1":{"accentHue":0-360,"bgHue":0-360,"bgLightness":0-100,"textLightness":0-100}, "mode2":{...}}`. Partial updates supported. Broadcasts ThemeChanged WebSocket event. - `theme.disablePinchZoomLock` (default: false) - When true, native pinch-to-zoom is unlocked on the dashboard (default is locked). Also surfaced as a toggle in the theme drawer. - `GET /system/qr` - QR code PNG for base URL (optional `?url=` override) - `POST /system/seed-resources` - Force re-extract all embedded resources (pages, charms) - `POST /system/client-log` - Browser-side log relay. Body: `{"level":"warn","msg":"...","page":"dashboard"}` (single event) or `{"events":[{...},{...}]}` (batch). Writes each event to the server `mbxhub.log` — dashboard pages should route operational telemetry here rather than `console.log` so problems in guest browsers are visible server-side. Recognized fields: `msg` (required), `level` (optional, default `info`, free-form string → server log levels, unknown = info), `page` (optional; wraps as `[page] msg`). Any OTHER fields on the event object are silently dropped — concatenate stack traces into `msg`. Both `msg` and `page` are sanitized (CR/LF/NUL stripped, 4096/256 char caps). Silent on empty body (returns `accepted:0`). Invalid JSON → `400 INVALID_REQUEST` (note: this endpoint uses `INVALID_REQUEST`, not `INVALID_JSON` — it predates the topology PUT handler's specific code). Response: `{success:true, data:{accepted:N}}`. **Access:** `ActionCategory.System` (admin-only). PartyMode guests are rejected so guest-sourced log entries can't mix into operator telemetry. ## Network Discovery MBXHub advertises on the local network via three protocols, all controlled by the `discoveryEnabled` setting: - **SSDP/UPnP** (UDP 1900) — UPnP device advertisement - **WS-Discovery** (UDP 3702) — Windows Network folder integration - **mDNS/DNS-SD** (UDP 5353) — Bonjour/zero-conf (Win10 1809+, graceful fallback on older) The `discoveryName` setting controls the friendly name shown across all protocols. Empty = machine name. - `GET /device.xml` - UPnP device description XML (friendlyName, presentationURL, services) - `POST /wsd` - WS-Discovery metadata exchange. Windows sends SOAP GetMetadata after UDP Probe discovery; response includes PresentationUrl → `/dashboard`. This makes MBXHub appear in the Windows Explorer Network folder with a "Device webpage" link. Firewall: TCP (REST port) + UDP 1900,3702,5353 must be open. UDP 5353 is mDNS (device discovery via raw UDP multicast). ## PartyMode Web-based party music system for group listening. Three roles: Guest (browse/request), DJ (full control), Display (TV mode). ### Pages - `GET /pages/partymode/` - Entry point with PIN + nickname form - `GET /pages/partymode/guest.html` - Guest: browse 7 tabs (Albums/Artists/Genres/Playlists/Podcasts/Radio/Moods), fuzzy search, request songs, vote, react - `GET /pages/partymode/dj.html` - DJ: full playback control, drag-drop queue reorder, AutoQ control - `GET /pages/partymode/display.html` - TV: large artwork, lyrics, floating reactions, request feed - `GET /pages/partymode/leaderboard.html` - Party stats: top guests, top tracks, reaction counts ### Endpoints - `GET /partymode/status` - Check if party is active - `POST /partymode/start` - Start party (body: {"guestPin":"1234","djPin":"5678"}) - `POST /partymode/stop` - End party session - `GET /partymode/validate?pin=1234&nickname=Haro` - Validate PIN, register join, returns role (guest/dj) - `POST /partymode/verify-dj` - Verify DJ PIN for DJ page access (body: {"pin":"5678"}) - `POST /partymode/vote` - Submit vote with attribution (body: {"type":"++","target":"Artist","value":"Rock","nickname":"Haro"}) - `POST /partymode/request` - Submit song request (body: {"url":"path","nickname":"Haro"}) - `GET /partymode/requests` - Recent requests (for DJ view) - `GET /partymode/feed` - Activity feed: joins, requests, votes, and reactions (for display) - `GET /partymode/qr` - Generate QR code image (PNG) with party URL - `GET /partymode/role` - Current user's role (guest/dj/anonymous) + QR sharing flags ### QR Code Sharing - `GET /system/qr` returns QR for base URL; `?url=` overrides target - Guest and DJ pages have QR buttons for viral party sharing - Dashboard QR auto-switches to party join URL when party is active - Settings: `disableGuestQr` hides QR on guest page, `disableHostQr` hides QR on DJ/dashboard ### Quick Start 1. DJ visits `/pages/partymode/`, starts party with PIN 2. Display page shows QR code with embedded PIN 3. Guests scan QR, enter nickname, browse and request songs 4. Requests appear on DJ page and display feed ### Integration with Influences Guests can vote (thumbs up/down) on currently playing and queued tracks. Uses the Influences API to adjust shuffle preferences in real-time. ## AutoQ (v0.4.9+) Intelligent queue system. AutoQ combines TrueShuffle rules, mood analysis, reactions, and influences to automatically queue tracks that match the room's energy. ### TrueShuffle (Rules Engine) TrueShuffle manages the shuffle cycle — play rules, cycle tracking, and queue constraints. Returns `503 SERVICE_UNAVAILABLE` if TrueShuffle/AutoQ not enabled. - `GET /shuffle/status` - Shuffle cycle progress - `POST /shuffle/reset` - Reset shuffle cycle - `GET /shuffle/played` - Tracks played this cycle - `GET /shuffle/remaining` - Tracks remaining ### Banlist Permanently excluded tracks. Banned tracks are never queued by AutoQ. Returns `503 SERVICE_UNAVAILABLE` if TrueShuffle/AutoQ not enabled. - `GET /banlist` - List banned tracks - `POST /banlist` - Ban track (body: {"url":"path","reason":"optional"}) - `DELETE /banlist/{url}` - Unban track ### Influences Influence rules shape AutoQ scoring — Pandora-style thumbs up/down on artists and genres. Unlike bans (permanent, track-specific), influences are resettable metadata preferences. Returns `503` if TrueShuffle/AutoQ not enabled. - `GET /influences` - List influences - `POST /influences` - Add influence (body: {"type":"++","target":"Genre|Artist","value":"Rock"}) - `DELETE /influences/{target}/{value}` - Remove influence - `POST /influences/clear` - Clear all influences - `GET /influences/current` - Current track's influence data ### Scoring System - **Reactions** on now playing: Fire (+3, triggers queue refresh), Heart (+2), Like (+1), Dislike (-1), Ban (-100, excludes). Reactions auto-create influences: fire/heart → artist++, like → genre++, dislike → genre--, ban → artist-- - **Influences**: Thumbs up/down on artists and genres (also created automatically from reactions) - **Recency**: Small boost for recently reacted tracks - **Mood Matching**: Tracks matching target mood score better ### Mood Channels Default mood channels with arousal/valence targets (customizable via `autoQ.moodChannels` in mbxhub.json): - Energetic (0.95, 0.85) | Dance (0.80, 0.70) | Intense (0.90, 0.20) | Upbeat (0.65, 0.85) - Morning (0.50, 0.80) | Chill (0.30, 0.70) | Mellow (0.20, 0.50) | Night (0.15, 0.30) - Emotional (0.45, 0.20) | Relax (0.10, 0.60) Custom channels example in mbxhub.json: ```json "moodChannels": [ { "name": "Energetic", "emoji": "🔥", "arousal": 0.90, "valence": 0.80 }, { "name": "Chill", "emoji": "😌", "arousal": 0.35, "valence": 0.65 } ] ``` ### Mood Quadrants Arousal (energy) is the vertical axis, valence (positivity) is the horizontal. Each mood channel targets a point in this 2D space. - **High arousal + high valence** = Energetic, upbeat (EDM, pop, funk). Fast tempo, bright timbre, strong beats. - **High arousal + low valence** = Tense, aggressive (metal, hard rock, industrial). Distortion, high energy, dissonance. - **Low arousal + low valence** = Sad, subdued (ambient drone, slow blues, lo-fi). Slow tempo, dark timbre, soft dynamics. - **Low arousal + high valence** = Calm, pleasant (chillhop, acoustic folk, soft jazz). Warm timbre, consonance, smooth textures. ### AutoQ Settings Key settings in `mbxhub.json` under `autoQ`: - `pickMode` (default: "Weighted") - Track selection mode: Off (shuffle fill only), Favorites (highest-scored), Weighted (score-proportional random), Random (uniform random, diversity caps still apply) - `artistQuota` (default: 1) - Max tracks from same artist in batch (0=disabled) - `genreQuota` (default: 3) - Max consecutive same-genre tracks (0=disabled) - `moodMatchWeight` (default: 0.4) - Weight for mood matching in scoring (0-1) - `moodTagField` (default: "Custom1") - MusicBee custom tag field for writing mood labels (null=disabled) - `moodTagFieldName` (default: "AutoQ Mood") - Expected display name for the custom tag field (must match MusicBee config) - `minReplayMinutes` (default: 30) - Minimum minutes before a track can be replayed - `diversityWindowSize` (default: 10) - Recent tracks considered for diversity calculations - `minSessionEntropy` (default: 0.5) - Entropy threshold before boosting diversity (0-5) ### Estimation Settings Normalization parameters for mood matching and feature extraction, tunable via `autoQ.estimation` in mbxhub.json: - `usePercentileNormalization` (true) - Use percentile-based normalization (library-adaptive). When false, uses absolute min/max ranges below. Requires >= 5 Essentia-analyzed tracks. - `moodMatchMinSimilarity` (0.5) - Minimum similarity to match a mood channel - `bpmMin` (80), `bpmMax` (170) - BPM normalization range (used when percentile off) - `ratingScale` (5.0) - Rating scale for valence normalization - `yearMin` (1950), `yearMax` (2030) - Year normalization range - `playCountLogDivisor` (4.0) - Play count log scaling divisor - `moodComboMaxResults` (2) - Max mood-combo channels returned per track (range 1-5) - `moodComboMinScore` (0.85) - Min similarity to include a mood channel in combo results (0-1) - **Valence weights** (positivity, sum ~1.0) under `autoQ.estimation`: `valenceWeightMode` (0.35), `valenceWeightDissonance` (0.25), `valenceWeightChords` (0.15), `valenceWeightPitchSalience` (0.10), `valenceWeightMfcc` (0.10), `valenceWeightDance` (0.05), `valenceWeightCentroid` (0.0 — centroid is an arousal feature), `valenceWeightFlatness` (0.0 — always zero from Essentia). Mode scoring: `modeScoreMajor` (0.85), `modeScoreMinor` (0.4). - **Arousal weights** (energy, sum ~1.0) under `autoQ.estimation`: `arousalWeightBpm` (0.25), `arousalWeightLoudness` (0.20), `arousalWeightFlux` (0.15), `arousalWeightOnsetRate` (0.15), `arousalWeightRms` (0.10), `arousalWeightZcr` (0.08), `arousalWeightCentroid` (0.05), `arousalWeightDynamicRange` (0.05), `arousalWeightDance` (0.02). `DynamicRange` is BS.1770 LRA in LU from Essentia; when null, `MoodComputer` falls back to genre-based defaults at lookup time (metal→6, jazz→13, classical→17, unknown→8). - **Normalization ranges** (used when percentile off) under `autoQ.estimation`: `centroidMin/Max` (500/2300 Hz), `loudnessMin/Max` (-23/-5 dB), `onsetRateMax` (5.5), `zcrMax` (0.12), `rmsMax` (0.008), `chordsRateMax` (0.2), `fluxMax` (0.12), `danceMax` (1.7), `mfccMin/Max` (70/220). ### Endpoints - `GET /autoq/status` - AutoQ status and configuration - `POST /autoq/start` - Start AutoQ (body: `{"mode":"autopilot"|"djassist"}`) - `POST /autoq/stop` - Stop AutoQ - `GET /autoq/moods` - Get available mood channels with arousal/valence coordinates - `GET /autoq/moods/browse?channel=Energetic` - Browse tracks matching a mood channel (?limit=200) - `GET /autoq/track-mood` - Raw mood data for current track (or ?url=file://...). Returns file, album, Essentia features, percentile ranks, computed valence/arousal, best mood match, confidence score (0-1), genre profile applied - `POST /autoq/mood` - Set target mood (body: {"channel":"Energetic"}) - `POST /autoq/moods/reload` - Reload mood cache from disk - `GET /autoq/mood-cache/status` - MetaServer mood-cache warm-up visibility. Response: `{total, essentia, fallback, warmupInProgress, lastWarmup: {at, ran, skipReason, librarySize, fromLocalFile, fromMetaServer, stillMissing, durationMs} | null}`. `lastWarmup` is null when no warm-up has run yet (e.g. MetaServer disabled). Clients can subscribe to `MoodCacheWarmed` WS event for push updates instead of polling. - `GET /autoq/vibe-list` - Current candidate tracks with scores (?count=50) - `POST /autoq/react` - Submit reaction (body: {"emoji":"fire","nickname":"Haro"}) - `GET /autoq/reactions` - Reaction history (?trackUrl=, ?limit=50) - `GET /autoq/stats` - Leaderboard data (top guests, top tracks, reaction breakdown) - `GET /autoq/taste-explorer` - Discover tracks adjacent to current taste profile, grouped by category (?limit=100&groupBy=auto|genre|artist|mood) - `GET /autoq/similar` - Find tracks similar to a seed track by metadata and feature distance (?url={trackUrl}&limit=20) - `GET /autoq/settings` - All tunable AutoQ parameters (weights, scores, normalization, genre profiles, confidence thresholds) - `PUT /autoq/settings` - Partial update AutoQ parameters (body: JSON with changed fields only). Changes apply on next scoring pass. Supports genreProfiles dictionary for genre-aware weight adjustment. - `POST /autoq/retag-moods` - Bulk write mood tags to MusicBee custom field (Essentia-analyzed tracks only) - `POST /autoq/vibe-list/refresh` - Force refresh vibe list - `POST /autoq/pick` - Pick next track from vibe list (returns best track without queueing) - `POST /autoq/unban` - Unban a track (body: {"url":"file://..."}) - `GET /autoq/banned` - Check if current track is banned - `POST /autoq/reset` - Reset AutoQ session state (clears reactions, taste vector, ban list) ### Reactions - `fire` - This track is fire! (+3, triggers queue refresh) - `heart` - Love this song (+2) - `like` - Good choice (+1) - `dislike` - Not feeling it (-1) - `ban` - Skip and exclude (-100) ### Quick Start 1. Enable AutoQ in MBXHub settings or via `POST /autoq/start` 2. Guests react to now playing tracks via guest page or `/autoq/react` 3. AutoQ learns preferences and adds matching tracks when queue runs low 4. View leaderboard at `/pages/partymode/leaderboard.html` or via `/autoq/stats` 5. Tune algorithm live at `/pages/autoq.html` - mixer-style sliders for all weights, scores, and normalization ranges ## Access Control (v0.4.8+) ### Roles - **DJ** - Full playback control, queue management, see requests. Authenticated via DJ PIN. - **Guest** - Browse library, request songs, vote on vibes. Authenticated via Guest PIN. - **Anonymous** - Unauthenticated access. Subject to protection level restrictions. ### Protection Levels - **Default** - Normal operation. Granular read-only controls available (player, queue, library, playlists). - **Kiosk** - All navigation redirects to default page. For party displays or public terminals. - **Read-Only** - Master switch blocks all write operations. PartyMode endpoints exempt. ### PIN Authentication PartyMode uses PIN-based authentication: - `GET /partymode/validate?pin=1234&nickname=Haro` - Validate PIN, returns role (dj/guest) - `POST /partymode/verify-dj` - Verify DJ PIN (body: {"pin":"5678"}) - `X-Party-PIN` header - Include PIN in request header for authenticated API calls ### Access Control Response Blocked requests return 403 with error code: ```json {"success":false,"error":{"code":"READ_ONLY","message":"API is in read-only mode"}} {"success":false,"error":{"code":"PARTY_LOCKED","message":"Player controls locked during PartyMode"}} ``` ## Settings - `GET /settings` - All MusicBee settings - `GET /settings/storage-path` - Library storage path - `GET /settings/skin` - Current skin name - `GET /settings/field-name?field=N` - Field display name - `GET /settings/data-type?field=N` - Field data type - `GET /settings/skin-element-color?element=N&state=N&is498=false` - Skin element color - `GET /settings/window-borders-skinned` - Whether window borders are skinned - `GET /settings/lastfm-user` - Last.fm username - `GET /settings/web-proxy` - Web proxy settings - `GET /settings/value?id=N&defaultValue=` - Get setting value by ID - `GET /settings/convert-command?format=N` - Conversion command line ## Podcasts - `GET /podcasts` - List subscriptions - `GET /podcasts/{id}` - Subscription details - `GET /podcasts/{id}/artwork` - Subscription artwork - `GET /podcasts/{id}/episodes` - List episodes (also: `GET /podcasts/episodes?id={url}` for feed URL IDs) - `GET /podcasts/{id}/episodes/{index}` - Episode details ## Pending - `GET /pending` - Pending file info - `GET /pending/url` - Pending file URL - `GET /pending/property?type=X` - Pending file property - `GET /pending/tag?tag=X` - Pending file tag ## MusicBee App - `POST /app/exit` - Exit MusicBee (requires allowRemoteExit). Optional body: `{"restart":true,"delay":22}` to schedule restart via Task Scheduler - `POST /app/restart` - Restart MusicBee (requires allowRemoteExit). Optional body: `{"delay":22}`. Convenience alias for exit with restart=true - `GET /mb/window-handle` - Window handle - `POST /mb/refresh-panels` - Refresh UI panels - `POST /mb/command` - Invoke MusicBee command - `GET /mb/visualisers` - List visualizers - `POST /mb/visualiser` - Show visualizer - `POST /mb/plugin-view` - Show plugin view - `GET /mb/plugin-views` - List plugin views - `GET /mb/localisation?id=X` - Get localized string - `POST /mb/nowplaying-assistant` - Show now-playing assistant panel - `POST /mb/filter` - Open filter in MusicBee (body: {"filter":"..."}) - `POST /mb/window-size` - Set window size (body: {"width":800,"height":600}) - `POST /mb/download` - Download file (body: {"url":"..."}) ## RPC Direct access to all 137 MusicBee API methods. Subject to the same read-only and granular access controls as REST endpoints (e.g., `Player_PlayPause` requires player playback permission, `Library_SetFileTag` requires library tags permission). - `POST /rpc/{methodName}` - Call any API method Example: ``` POST /rpc/Player_PlayPause POST /rpc/NowPlaying_GetFileTag (body: {"tag":"Artist"}) POST /rpc/Library_QueryFiles (body: {"query":"genre=Rock"}) ``` Method categories: Player_*, NowPlaying_*, NowPlayingList_*, Library_*, Playlist_*, Setting_*, MB_*, Podcasts_*, Sync_* **GOTCHA:** After writing tags via RPC, call `MB_RefreshPanels` or MusicBee UI won't update: ``` POST /rpc/Library_SetFileTag (body: {"file":"...","tag":"Rating","value":"5"}) POST /rpc/Library_CommitTagsToFile (body: {"file":"..."}) POST /rpc/MB_RefreshPanels ← Don't forget this! ``` Note: REST endpoints (PUT /library/file/...) call refresh automatically. ## Scan (Auto Mood Analysis) Automatic Essentia analysis pipeline. When a track changes, the plugin checks the current track and upcoming queue tracks. Tracks are enqueued for scan in two cases: (a) no Essentia data at all in the local cache, (b) v0.5.2.4+: data exists but lacks the 39-field extended schema (detected via SpectralRolloff presence) — playing an old-scan track triggers a quiet background rescan so the new fields backfill without interrupting playback. Missing/partial tracks broadcast as `ScanEnqueue` WebSocket events. Shell's ScanWorker (or a remote peer via ScannerPool) picks them up, runs `truedat.exe` with inline Essentia + fileMd5 + audioMd5 + chromaprint, and posts results to MetaServer. Flow: TrackChanged → plugin broadcasts ScanEnqueue (priority high=current, normal=look-ahead) → Shell ScanWorker → truedat.exe → MetaServer ingest Self-disabling if Truedat/Essentia not found on the Shell machine. ### REST Endpoints - `GET /scan/status` - Mood cache stats and auto-scan config. Response: `{"success":true,"moodCache":{"total":N,"essentia":N,"fallback":N},"autoScan":{"enabled":bool,"lookAhead":N}}` - `POST /scan/track` - Enqueue a single track or album for analysis. Body: `{"file":"path"}` and/or `{"album":"name"}`. Optional `"force":true` bypasses the has-data guard. **Phase 2 optional `"mode":"fingerprint"|"stream"|"essentia"`** (default essentia) — `fingerprint` and `stream` skip Essentia entirely and invoke `truedat --hash-only --level X`, producing identity signals only (fingerprint.v1 and/or audioStreamSha256). Hash-only modes also bypass the has-data guard since identity is orthogonal to features. Does not require autoScan.enabled. Broadcasts ScanEnqueue events with `mode` attached. Response: `{"success":true,"enqueued":N}`. Invalid mode → 400 INVALID_MODE. - `POST /scan/fingerprints` - **Phase 2.** Gap-detection sweep: enumerates the library, queries MetaServer identity coverage in 200-path chunks, enqueues ScanEnqueue(mode=fingerprint) for every track without `identity.fingerprint.v1`. Idempotent — tracks already fingerprinted are skipped. Body (optional): `{"mode":"fingerprint"|"stream"}` (default fingerprint). Responds 200 **immediately** with `{"success":true,"enqueuing":true,"totalFiles":N,"mode":"fingerprint"}` (prevents stalling the plugin HTTP thread on large libraries); the probe loop + per-missing broadcasts run on a ThreadPool task with periodic `Task.Yield`. Final `scanned/missing/enqueued/elapsedMs` counts are logged under `HandleFingerprints background sweep`. Empty library returns `{"success":true,"enqueued":0,"totalFiles":0}` (no sweep). 503 if MetaServer not resolved. - `POST /scan/unanalyzed` - Bulk-enqueue all unanalyzed library tracks. Requires autoScan.enabled. Broadcasts ScanEnqueue events. Responds 200 immediately with `{"success":true,"enqueuing":true,"totalFiles":N}`; enumerate-and-broadcast loop runs on a ThreadPool task so the HTTP thread isn't stalled on large libraries. ### WebSocket Events - `ScanEnqueue` - Broadcast when a track needs analysis. Payload: `{"file":"path","priority":"high|normal","mode":"essentia|fingerprint|stream","share":"UNC path (optional)","force":bool (optional)}`. `mode` defaults to `essentia` (full feature extraction); Phase 2 sweeps use `fingerprint` or `stream` for hash-only identity-only scans. `share` is present when `syncLibraryPath`+`syncSharePath` are configured. `force=true` bypasses ScanWorker's `_pending`/`_failed` gates. - `ScanCancel` - Broadcast to cancel a pending scan item. Payload: `{"file":"path"}` ### Configuration Plugin settings (mbxhub.json → autoScan): - `enabled` (bool, default true) - Master on/off for automatic scanning - `lookAhead` (int, default 5) - Number of upcoming queue tracks to pre-scan Shell settings (mbxhub-shell.json → scan): - `mode` (string, default "auto") - Scan mode: `off` disables scanning, `local` forces local-only, `auto` uses ScannerPool to discover and route to available scanners (local or remote) - `includeLocal` (bool, default true) - Include this machine in the auto scanner pool when mode is `auto` - `preferredUrl` (string, null) - Preferred remote scanner MetaServer URL, given priority in `auto` mode. Example: `"http://scanner:8081"`. - `batchSize` (int, default 1) - Items per truedat invocation (1-32). Controls local parallelism. - `truedatPath` (string, null=auto-discover) - Path to truedat.exe - `cpuLimit` (int, default 25) - CPU percentage limit for Essentia (1-100) Scan routing: the ScannerPool selects a scanner on each request. `preferredUrl` is tried first in `auto` mode; if unavailable, falls back to local. Set `mode: "local"` to force local-only regardless of peers. Remote scanning: configure `syncLibraryPath` + `syncSharePath` in mbxhub.json (root-level settings). ScanEnqueue events include a `share` field with the UNC path for remote file access. Remote Shell (scanner) opens files via UNC, runs Essentia analysis, and delivers features back via PeerSync. Scanner requires zero path configuration — host owns the mapping. ## Sync (Preview) Library sync between MBXHub instances (coming in .6 LP): - `GET /sync/status` - Sync status - `GET /sync/peers` - Known peers - `GET /sync/discover` - Discover peers on network - `GET /sync/delta` - Library delta since last sync - `GET /sync/operations` - All sync operations - `GET /sync/operations/{syncId}` - Single sync operation by ID - `POST /sync/start` - Start sync operation - `POST /sync/stop` - Stop sync - `POST /sync/pull` - Pull files from peer - `POST /sync/push` - Push files to peer ### Sync Settings Settings in `mbxhub.json`: - `syncEnabled` (default: false) - Enable library sync - `syncRole` (default: "island") - Sync role: "island" (standalone), "hub", or "spoke" - `syncLibraryPath` (default: "") - Path to local library for sync operations - `syncSharePath` (default: "") - Shared path for peer file transfer ## Device Sync (MBSync) Internal MusicBee device synchronization callbacks. Used by MusicBee's built-in sync feature when syncing with mobile devices: - `POST /mbsync/file/start` - Notify sync file transfer starting - `POST /mbsync/file/end` - Notify sync file transfer complete - `POST /mbsync/file/delete/start` - Notify sync file deletion starting - `POST /mbsync/file/delete/end` - Notify sync file deletion complete ## Debug Diagnostic endpoints for testing and validation (used by MBXHVAL test toolkit): - `GET /test/websocket` - Interactive WebSocket event test page with subscription controls - `GET /debug/clouseau/status` - Clouseau inspector plugin status - `GET /debug/clouseau/state` - Get Clouseau state snapshot - `POST /debug/clouseau/state` - Save Clouseau state - `GET /debug/clouseau/files` - Get Clouseau file list ## Response Format Success: ```json {"success":true,"data":{...}} ``` Error: ```json {"success":false,"error":{"code":"ERROR_CODE","message":"Description"}} ``` ## Usage Rules ### File URLs All file URLs are Windows paths. URL-encode when passing in path parameters: - Original: `C:\Music\Artist\Track.mp3` - Encoded: `C%3A%5CMusic%5CArtist%5CTrack.mp3` ### Pagination Most list endpoints support `offset` and `limit` query parameters: - Default limit: 50 - Maximum limit: 10000 - Maximum offset: 1000000 - Example: `GET /library/files?offset=100&limit=50` ### Sorting Library endpoints support `?sort=` parameter for server-side sorting: - `alpha` (default) - Alphabetical by title+artist - `artist` - By artist name, then title - `album` - By album name, then track number - `title` - By title only - `date` - By date added (newest first) - `track` - By disc number, then track number (natural album order) - `name` - By display name - `year-asc` - By year ascending (chronological, oldest first) - Example: `GET /library/files?artist=Pink+Floyd&sort=album` ### Request Limits - Maximum request body size: 1 MB - Content-Type must be `application/json` for POST/PUT with body ### CORS Policy - `localhost`, `127.0.0.1` - Always allowed - `192.168.x.x`, `10.x.x.x`, `172.16-31.x.x` - Allowed when remote connections enabled - External origins - Blocked ### Access Control (Read-Only Mode) MBXHub can restrict write operations via settings: - **Master read-only**: Disables all write operations API-wide - **Granular controls**: Player, Queue, Library, Playlists can be individually restricted - **PartyMode exempt**: PartyMode endpoints always work (voting, requests, etc.) Blocked requests return: ```json {"success":false,"error":{"code":"READ_ONLY","message":"API is in read-only mode"}} ``` Handle in code: ```javascript const res = await fetch('/player/play', {method:'POST'}); if (res.status === 403) { const json = await res.json(); if (json.error?.code === 'READ_ONLY') { showMessage('Controls are locked'); } } ``` ### Security Settings Key security settings in `mbxhub.json`: - `apiReadOnlyMode` (default: false) - Master read-only switch. Blocks all write operations API-wide. - `disableRemoteConfig` (default: false) - Block remote access to settings schema and config updates. Localhost always allowed. - `protectMetadata` (default: true) - Block metadata writes (love, rate, tag edits) for all users including DJ - `rateLimitEnabled` (default: true) - Enable rate limiting for PartyMode endpoints - `rateLimitRequestsPerMinute` (default: 5) - Max song requests per IP per minute - `rateLimitVotesPerMinute` (default: 5) - Max votes per IP per minute - `rateLimitPinAttemptsPerMinute` (default: 5) - Max PIN validation attempts per IP per minute - `trustForwardedFor` (default: false) - Trust X-Forwarded-For header for client IP detection. Only enable if behind a reverse proxy that sets this header. - `ariaEnabled` (default: false) - ARiA input simulation. When disabled, all /aria endpoints return 403. - `ariaAllowedPrograms` - Allowlist for ARiA run() command. Only listed programs can be launched. Do not add shell interpreters (cmd.exe, powershell.exe) as they bypass the allowlist. ### IP Filtering Control which IP addresses can connect when remote connections are enabled: - `filteringMode` (default: "All") - All (any IP), Range (subnet range), or Specific (whitelist) - `filterBaseIp` - Base IP for Range mode (e.g., "192.168.1.") - `filterLastOctetMax` (default: 254) - Max last octet for Range mode - `filterAllowedIps` - Array of allowed IPs for Specific mode Example Range mode (allow 192.168.1.1 to 192.168.1.50): ```json {"filteringMode": "Range", "filterBaseIp": "192.168.1.", "filterLastOctetMax": 50} ``` Example Specific mode (whitelist): ```json {"filteringMode": "Specific", "filterAllowedIps": ["192.168.1.10", "192.168.1.20"]} ``` ### Dashboard Footer Links Configure which links appear in the dashboard footer via `dashboardFooterLinks` in mbxhub.json: - `dashboardFooterLinks` (default: null) - Null = built-in defaults (Player, Pages, QR*, RDP*, API*). Array of `{label, url, enabled}` for custom links. - Conditional visibility: `/system/qr` requires `restEnabled`, `/remoteapp/rdp` requires `remoteAppEnabled` + `!remoteAppApiDisabled`. - The footer is a dashboard layout section (`footer`) — hide/show and reorder via `dashboardLayout`. ### Dashboard Layout Configure which dashboard panels are shown, their order, and which are collapsible via `dashboardLayout` in mbxhub.json: - `order` (default: ["status","search","nowplaying","rating","controls","volume","charms","mood","playlists","toggles","footer"]) - Render order of sections - `hidden` (default: []) - Section IDs to hide entirely - `collapseAfter` (default: 5) - First N visible sections always shown; rest go in collapsible group - `nowPlayingStyle` (default: "full") - Server-side default NP style (see Now Playing Styles) - `immersiveFadeDelay` (default: 5000, range 0-10000) - Immersive overlay fade delay in ms (0 = always visible) - `immersiveTextSize` (default: "M") - Now-playing metadata text size. Despite the name, applies to *all* NP styles — not just immersive. Options: S, M, L, XL. - `statusBarSize` (default: "S") - Status bar size: S (thin, default), M (medium), L (large) - `searchBarSize` (default: "S") - Search bar size: S (thin, default), M (medium), L (large) - `controlsSize` (default: "M") - Transport control button size: S, M, L, XL, XXL - `progressBarSize` (default: "M") - Progress bar thickness: S (4px), M (6px), L (10px) - `disableVolumeGesture` (default: true) - Disable vertical swipe-on-artwork gesture for volume control. On by default (true) to avoid conflicts with page scroll; horizontal track-skip gestures are unaffected. - `disableExploreNewWindow` (default: true) - When true, Explore opens in the same tab instead of a new window. Section IDs: `status` (status bar), `search` (library search), `nowplaying` (track info/artwork), `rating` (stars/ban/love), `controls` (prev/play/next), `volume` (+/-/mute), `playlists` (playlist selector), `toggles` (shuffle/repeat), `mood` (mood/reactions), `charms` (charm bar), `footer` (footer links) ### Now Playing Styles - `nowPlayingStyle` (default: "full") - Server-side default. Options: `full`, `horizontal`, `noart`, `split`, `immersive`. - Client-side override: localStorage key `mbxh_np_style` overrides the server default per-device. Header button cycles through all styles. - `split` — Two-column grid (art left, metadata right). Column ratio configurable via localStorage `mbxh_split_ratio` (default 55, range 40-70). - `immersive` — Full-bleed album art with gradient overlay and blurred letterbox fill for non-square artwork. Metadata fades in on hover/touch, fades out after delay. Delay configured via server setting `dashboardLayout.immersiveFadeDelay` (default 5000ms, `0` = always visible, range 0 or 1000-10000). - All styles have zoom-level overrides (67%, 42%, 27%). Split falls back to stacked below 480px. Immersive reduces art height below 400px. - **Removed in v0.5.2.3**: the `compact` style. Existing configs referencing `compact` gracefully fall back to `full`. ### Dashboard Display Settings - `hideFileInfo` (default: false) - Hide file format info (codec, sample rate/bitrate) and hi-res badge from the now-playing section. Hi-res badge appears when sample rate exceeds 44.1kHz. - `hideInfluenceControls` (default: false) - Hide thumbs up/down influence buttons in the now-playing section. - `hideStatusMessage` (default: false) - Hide the status bar message after dashboard actions. - `disableExploreButton` (default: false) - Hide the Explore button in transport controls (`Disable*` is the canonical feature-toggle flag; renamed from `showExploreButton` in v0.5.2). Opens Explore view seeded to the currently playing album. - `hideSearchMoods` (default: false) - Hide mood quick-picks from search empty state. Moods section is also collapsible via click. ### Dashboard Search Behavior - Client-side fuzzy album/artist matching with server-side track search via `GET /library/search?q=` - Tabs: All, Artists, Albums, Tracks, Playlists. Tab ordering auto-sorts by best fuzzy-score match so the most relevant tab (e.g. Tracks for a song title) floats to the front. - Playlists tab uses the `p:` prefix filter against cached playlist names. Play/queue-next/queue-last actions via `POST /dashboard/playlist` (form: `playlistUrl=...`). - Albums tab also runs server-side search and boosts albums containing matching tracks (e.g. searching "wonderwall" surfaces the album) - Album results have inline track expansion (☰ Tracks button) showing numbered tracklist with per-track play/queue - Explore button on results opens explore.html seeded with `?album=&artist=` query params - Empty state shows collapsible mood pills and search history - Track result rows show `Title — Artist` when the track artist differs from the album artist (same convention in browse.html and explore.html expanded views) - `disableGuestQr` (default: false) - Hide QR code from guest/party mode dashboard. - `disableHostQr` (default: false) - Hide QR code from host dashboard. ### Feature Disable Settings Disable specific API features. When disabled, related endpoints return 404. Feature state (except proxy) is exposed via `GET /system/features`. - `apiDisableBanlist` (default: false) - Disable ban list endpoints (`/banlist/*`). - `apiDisableRatings` (default: false) - Disable rating controls (`/dashboard/rate/*`, `/dashboard/love`). - `apiDisableLoved` (default: false) - Disable love/heart tag feature. - `apiDisableReactions` (default: false) - Disable reactions/mood feature. - `apiDisableProxy` (default: false) - Disable device proxy endpoint (`POST /api/proxy`). - `disableStreaming` (default: false) - Disable audio/video streaming endpoint (`GET /stream/*`). ### MBXHub Shell (MBXHub.exe) Standalone companion providing Windows SMTC (System Media Transport Controls), MetaServer, firewall configuration, and more. The Shell connects to the plugin as a client but also hosts its own REST API (MetaServer) on a separate port. Plugin setting in `mbxhub.json`: - `startShellOnStartup` (default: false) - Launch MBXHub.exe when MusicBee starts. Plugin stops it on exit. Shell config in `mbxhub-shell.json` (next to MBXHub.exe): - `host` (default: "127.0.0.1") - MBXHub REST API host - `port` (default: 8080) - MBXHub REST API port - `wsPort` (default: 8080) - WebSocket port - `metaPort` (default: port + 1, e.g. 8081) - MetaServer REST API port - `meta.enabled` (default: true) - Enable MetaServer - `meta.allowRemote` (default: true) - Allow remote MetaServer connections - `meta.databasePath` (default: null) - Override MBXU database path (default: AppData/MBXHub/mbxu.db) - `retryIntervalMs` (default: 3000) - Retry interval on connection loss (ms) - `connectTimeoutMs` (default: 5000) - Connection timeout (ms) - `logLevel` (default: "Info") - Log verbosity: Trace, Debug, Info, Warn, Error - `logFile` (default: "mbxhub-shell.log") - Log file path (empty to disable) - `logDebugOutput` (default: false) - Write to debug output (DebugView/VS Output) - `debug` (default: false) - Debug mode: smaller log files, more archives Features: AUMID (`HALRAD.MBXHub`), auto-reconnect, single-instance enforcement. Uninstalling MBXHub via MusicBee cleans up AUMID, Start Menu shortcut, and Apps & Features entry. Requires .NET 8.0 Desktop Runtime. #### MBXHub.exe CLI - `MBXHub.exe` (no args) - Start SMTC bridge mode - `MBXHub.exe status` - Show system health and tool discovery status - `MBXHub.exe --no-smtc` - Run without SMTC bridge (headless/NAS mode) - `MBXHub.exe --install` - Register AUMID, create Start Menu shortcut, enable startup, report tool discovery - `MBXHub.exe --uninstall` - Remove AUMID registration, shortcut, and startup entry - `MBXHub.exe startup --enable` - Register auto-start at Windows logon (HKCU Run key, visible in Settings > Apps > Startup) - `MBXHub.exe startup --disable` - Remove auto-start registration - `MBXHub.exe shutdown` - Gracefully close all running Shell instances - `MBXHub.exe --detect` - Detect MusicBee installations - `MBXHub.exe --version` - Show version information - `MBXHub.exe --help` - Show help #### MBXHub.exe firewall Windows Firewall configuration (replaces standalone firebug.exe). Self-elevates via UAC when needed. - `MBXHub.exe firewall add --name MBXHub --tcp 8080,8081 --udp 1900,3702,5353 --urlacl 8080,8081` - Add firewall rules and URL ACL (8081 = MetaServer) - `MBXHub.exe firewall add --name MBXHub --port 8080 --urlacl` - Add single-port rule with URL ACL - `MBXHub.exe firewall remove --name MBXHub --port 8080` - Remove firewall rules and URL ACL - `MBXHub.exe firewall check --name MBXHub --port 8080` - Check if rules exist - `MBXHub.exe firewall status` - Show firewall and elevation status - `MBXHub.exe firewall open` - Open Windows Firewall settings UI #### MBXHub.exe database - `MBXHub.exe --reset-db` - Wipe all MetaServer data (mood features, identity signals, sync cursors). Database re-populates on next startup from mbxmoods.json and peer sync. ## MetaServer (MBXHub.exe — restPort + 1) Mood feature and identity database served by the Shell process (MBXHub.exe) on restPort + 1. Backed by MBXU SQLite. Accepts scan results from distributed scanners and serves mood data to the plugin. The plugin proxies all `/meta/*` requests to MetaServer — clients use the same base URL, no separate port needed. - `GET /meta/health` - Full system health: shell version, services, features, tool discovery (truedat, ffmpeg, ffprobe, fpcalc, essentia), MetaServer status, peer sync status, scan queue depth. Returns tool paths and versions when found, searched paths when not found. Scan section includes `mode`, `batchSize`, `healthy`, and `queueDepth`. - `POST /meta/discover` - Re-run tool discovery and refresh health status. Returns confirmation. - `GET /meta/capabilities` - Node capabilities for cross-instance SSDP discovery. Returns `node`, `version`, `capabilities[]` (e.g. `meta-server`, `scanner`, `smtc`, `peer-sync`), `endpoints`, and `scan` details (`mode`, `batchSize`, `healthy`, `queueDepth`). Used by PeerSync and plugin to discover what this Shell instance offers. - `GET /meta/config` - Returns current scan config keys: `scan.mode` (off|local|auto), `scan.includeLocal`, `scan.preferredUrl`, `scan.batchSize`, `scan.cpuLimit`. - `PUT /meta/config` - Updates one or more scan config keys. Body: `{"scan.mode":"auto","scan.includeLocal":true,"scan.preferredUrl":null,"scan.batchSize":1,"scan.cpuLimit":25}`. Persists to `mbxhub-shell.json`. Response: `{"status":"ok","applied":{...}}`. - `GET /meta/config/schema` - Returns SchemaEntry-format metadata for all Shell scan settings. Each entry: `key`, `type`, `category`, `tier` (0=basic, 1=advanced), `description`, `current`, `default`, and optional `min`/`max`/`options`. Used by the settings UI to build dynamic config panels. - `GET /meta/features?path={encoded-path}` - Single track mood feature lookup by file path. Fall-through identity walk: exact-path → path-tail (drive-agnostic) → audioMd5 → metadataKey. Response `features` block carries the 15 base fields plus the 39 nullable extended-Essentia fields (null when the source scan didn't capture them). - `GET /meta/features?audioMd5={md5}` - Single track lookup by decoded audio hash (identity.audioMd5 MediaMetadata lookup). - `GET /meta/features?artist={artist}&title={title}&album={album}&duration={seconds}` - Single track lookup by metadata (v0.5.2.4+: `album` added). Legacy peer-sync rows stored their `metadataKey` with `duration=0`; on miss, server retries with `duration=0` but only accepts the hit if the candidate row has a non-null, non-zero stored `DurationSeconds` — avoids 0.50-confidence false positives against placeholder rows. - `POST /meta/features/batch` - Batch lookup (max 500 queries). Body: `{"queries":[{"path":"...","artist":"...","title":"...","album":"...","duration":245},...]}`. Plugin's startup warm-up sends path + full metadata on every query so peer-sync rows (synthetic `peer://{bestId}` paths that won't match on path) get found via metadataKey. Each result returns the full 15 + 39 field set when matched. - `GET /meta/by-fingerprint/{audioHead64kMd5}` - **Phase 2.** Ms-scale peer probe: "do you have this file?" `audioHead64kMd5` is the 32-char hex MD5 of the first 64 KB at TagLib `InvariantStartPosition`, produced by `truedat --hash-only --level fingerprint`. Hit (200): `{match:true, audioStreamSha256: "...|null", hasFeatures, peerId}` — caller can short-circuit the Essentia scan and pull features from this peer. Miss (404): caller falls through to the next peer or enqueues a local scan. Invalid hex/length (400). - `GET /meta/by-hash/{audioStreamSha256}` - **Phase 2.** Durable byte-identity feature pull. `audioStreamSha256` is the 64-char hex SHA-256 of the metadata-stripped audio region, produced by `truedat --hash-only --level stream`. Hit (200) returns the full feature payload (`matchType=audioStreamSha256`, confidence 0.99) so the caller can ingest directly. Miss (404). - `POST /meta/ingest` - Scanner posts Essentia results. Body: `{"path":"...","metadata":{artist,title,album,genre,duration,fileSize},"identity":{fileMd5,audioMd5,audioStreamSha256,chromaprint,chromaprintDuration,"fingerprint.v1":{fileSize,pathTail,durationMs,sampleRate,channels,codec,bitrate,audioHead64kMd5,audioHead64kMd5Source?,codecRaw?}},"features":{...},"provenance":{scannedBy,tool,toolVersion,scannedAt,level?}}`. **Phase 2 hash-only modes**: `truedat --hash-only --level fingerprint|stream` POSTs the same envelope with `features` omitted and at least one identity signal populated. Identity is persisted as `identity.fingerprint.v1.*` sub-keys (audioHead primary-indexed) and `identity.audioStreamSha256` (primary-indexed) — primes fleet-wide ms-scale probes without paying the Essentia cost. **Inline hashes** (v0.5.2.4+): truedat's analyze mode now runs Essentia + fileMd5 + audioMd5 + chromaprint concurrently per track, so every ingest fills all three identity tiers in a single pass — no separate `--fingerprint` run needed. **Merge-on-ingest** (v0.5.2.4+): three-tier strategy. New row → full write. Existing all-zero garbage → full overwrite (recovery). Existing with data → merge (base scalars stay first-write-wins to avoid stomping good data; null extended-feature slots get filled from the incoming observation; DR upgrades when incoming source rank is strictly higher: `essentia-lra` > `derived-peak-rms` > `fallback-genre` > `fallback-none`). Enables peer rescans to converge without clobbering. The `features` block accepts the original 15 fields (bpm/key/mode/spectralCentroid/spectralFlux/loudness/danceability/onsetRate/zeroCrossingRate/spectralRms/spectralFlatness/dissonance/pitchSalience/chordsChangesRate/mfcc) plus 39 nullable extended fields added in v0.5.2.4: loudnessMomentary, loudnessShortTerm, dynamicRange (LRA in LU from Essentia `loudness_ebu128.loudness_range`, source tag `essentia-lra`), replayGain, silenceRate20dB/30dB/60dB, spectralRolloff/Complexity/Entropy/Kurtosis/Skewness/Spread/StrongPeak/Decrease, spectralEnergy + Low/MidLow/MidHigh/High bands, hfc, Bark/ERB/Mel crest+flatness+kurtosis+skewness+spread, beatsLoudness, chordsStrength, hpcpCrest, hpcpEntropy. Scanners send whatever they computed; null values stay null. - `POST /meta/import` - Import from mbxmoods.json or fingerprints.json. Body: `{"source":"mbxmoods.json","path":"C:\\path\\to\\file.json"}`. Returns job ID. - `GET /meta/import/{jobId}` - Poll import progress. Response includes state, processed/total counts, errors. - `GET /meta/export` - Full export of all tracks **with mood features** (legacy format, file download). Backs peer-sync. - `GET /meta/export?since={iso8601}` - Delta export — tracks with features added/updated since timestamp (v1.1 format with identity signals). **Scope boundary:** filters on `MoodFeatures != null`. Phase 2/3 identity-only rows (hash-only scans produce `fingerprint.v1` + `audioStreamSha256` with no features yet) are **not** exported here — peer-sync is the mood-data channel. Identity signals propagate on demand via `TrueUpScheduler` + `PeerPullService` (see `POST /meta/true-up`), not peer-sync. - `GET /meta/stats` - Database statistics (track counts, identity signals, confidence, DB size) - `GET /meta/queue?limit=N` - Snapshot of pending ScanWorker items in priority order. Operator visibility (Fleet panel consumes this via `/system/fleet/queue`). Returns 200 with empty array when worker is absent or idle (never 503). Response: `{workerStarted, queueDepth, returned, items:[{file, basename, share, priority}]}`. `limit` default 20, max 100. - `GET /meta/activity?since=SEQ&limit=N` - Recent scan `start`/`complete`/`failed` events newer than `since`. 200-event ring buffer with monotonic sequence cursor; clients advance `since = currentSeq` each poll for incremental updates. First poll with `since=0` seeds the cursor AND returns the last N events. `limit` default 50, max 200. Each event: `{seq, at, kind, file, basename, elapsedMs, error, dispatchedTo}`. `dispatchedTo` is self-reported by the executing ScanWorker (`Environment.MachineName`) so fleet-aggregated feeds show which node actually ran each scan — useful for visualizing dispatch flow when one peer routes work to another. Response: `{workerStarted, currentSeq, returned, events:[...]}`. **Shell-direct scan endpoints** (Shell's HTTP listener only — NOT reachable through the plugin's `/meta/*` proxy; hit the Shell URL directly, default `http://localhost:{restPort+1}`): - `GET /scan/failures` - Returns the Shell's persistent failed-file set. Used by the plugin's three enqueue paths (TrackChanged auto-scan, `/scan/unanalyzed`, proactive backfill) to mirror failures locally so they stop re-broadcasting ScanEnqueue for tracks the Shell already gave up on. Response: `{success:true, count:N, paths:[...]}`. 503 SERVICE_UNAVAILABLE if MetaService null; 500 INTERNAL_ERROR on load throw. - `POST /scan/reset-failed` - Wipes ScanWorker's in-memory + persisted failed-file set so every skipped track gets another try on the next sweep. No DB reset — only the failure table. Callers typically pair this with `POST /scan/unanalyzed` or `POST /scan/fingerprints` to force a full retry. - `POST /meta/sync` - Trigger immediate peer sync (SSDP discovery + pull from all peers) - `GET /meta/sync/status` - Current sync state (enabled, syncing, last result, known peers with names) - `POST /meta/scan` - Enqueue a file for analysis via the ScannerPool. Body: `{"file":"path","share":"UNC path (optional)","priority":"high|normal","force":bool (optional),"mode":"fingerprint|stream|essentia" (optional, Phase 2)}`. ScannerPool routes to local ScanWorker or a remote MetaServer. When `mode` is `fingerprint` or `stream`, ScanWorker shells out to `truedat --hash-only --level X` and posts identity-only IngestObservations — no Essentia, no features. Absent `mode` → essentia (pre-Phase-2 behavior). Returns 503 if no scan worker is available. - `POST /meta/identity-coverage` - **Phase 2.** Bulk identity coverage probe. Body: `{"paths":["...","..."]}` up to 500. Response: `{"success":true,"coverage":[{"path":"...","known":bool,"hasFingerprint":bool,"hasStreamSha":bool,"hasFeatures":bool},...]}`. Plugin's gap-detection sweep uses this to find tracks missing `identity.fingerprint.v1` or `identity.audioStreamSha256` without issuing N single-path queries. - `GET /meta/features/by-fingerprint/{audioHead64kMd5}` - **Phase 2.** Fingerprint-keyed feature pull. 32-hex `audioHead64kMd5`. Peer uses this after `/meta/by-fingerprint` confirms a match — fetches the full feature payload without needing the peer's local path. Response: `{success, matched, matchType:"fingerprint.v1", confidence:0.9, features, identity, peerId}`. Miss → 404. - `POST /meta/true-up` - **Phase 2.** One-pass true-up sweep. Walks local tracks that have `identity.fingerprint.v1` but no `MoodFeatures`, attempts peer-pull for each via `/meta/by-fingerprint` → `/meta/features/by-fingerprint`. Optional `?limit=N` (default 500, max 5000). Response: `{"success":true,"scanned":N,"pulled":N,"missed":N,"limit":N}`. 503 PEER_PULL_UNAVAILABLE if no PeerSync. Cheap self-healing — orders of magnitude less work than an Essentia rescan, safe to run on a schedule. Background scheduler lives in `mbxhub-shell.json` under `meta.trueUp`: `{enabled:false, intervalMinutes:60 (min 5), batchLimit:500 (1-5000)}`. Off by default. - `GET /meta/fingerprints?since={iso8601}&limit={N}` - **Phase 2.** Paginated `identity.fingerprint.v1.audioHead` index dump. Used by joining peers and content-addressed dispatch to learn which tracks this node has cataloged. `since` filters by `MediaItem.ImportedAt`; `limit` default 500, max 5000. Response: `{"success":true,"signal":"fingerprint","count":N,"done":bool,"next":"iso8601|null","items":[{"mediaItemId","path","value","importedAt"},...]}`. Paginate by passing previous `next` as the new `since` until `done:true`. Skips `peer://` synthetic paths. - `GET /meta/hashes?since={iso8601}&limit={N}` - **Phase 2.** Paginated `identity.audioStreamSha256` index dump. Same shape as `/meta/fingerprints`, different key — the durable byte-identity tier. - `POST /meta/dedup` - Collapse peer-sync duplicate rows by grouping on `identity.metadataKey`, keeping a canonical (prefer non-peer:// path, then latest `MoodFeatures.AnalyzedAt`), and deleting the rest. Also deletes orphan `peer://` rows with no identity signals at all. Manual endpoint runs VACUUM to reclaim disk. Response: `{"success":true,"data":{"backfilledSignals":N,"mergedDuplicates":N,"deletedOrphans":N,"remainingTracks":N}}`. **Auto-dedup on startup** (v0.5.2.4+): MetaHost runs `AutoDedupIfStale(7d)` after `BackfillMetadataKeySignals` at every start; skips VACUUM (startup stays snappy), idempotent via `SyncMetadata['dedup.appliedAt']`, swallows errors so a dedup fault can't block MetaServer from coming up. ### SMTC Target Switching Runtime SMTC target management — discover MBXHub endpoints on the network and switch which one the Shell mirrors. - `GET /meta/smtc/target` - Current SMTC target (host:port) and connection state - `PUT /meta/smtc/target` - Switch SMTC target. Body: `{"target":"host:port"}`. Tears down existing WS connection, updates config, reconnects to new endpoint. - `GET /meta/smtc/endpoints` - Cached list of discovered MBXHub endpoints on the network. Each entry: address, name, active (boolean), capabilities. Returns 503 if SMTC bridge unavailable. - `POST /meta/smtc/endpoints/refresh` - Clear cache and re-scan network via SSDP for MBXHub endpoints. Returns fresh endpoint list. ### Identity Matching (5-tier) Lookups walk a full identity chain regardless of query type: 1. **Path exact match** (confidence 1.0) — full path, case-insensitive (SQLite NOCASE), separator-normalized to backslash at ingest 1b. **PathTail match** (confidence 1.0) — last 3 path components lowercased (e.g. `artist\album\song.flac`). Drive- and root-agnostic: `C:\Users\scott\Music\Artist\Album\Song.flac` matches `D:\Library\Artist\Album\Song.flac`. Indexed via `IX_MediaItems_PathTail`. This is the primary fix for cross-ingest mismatch where MetaServer rows were ingested from a different drive/user root than the plugin queries with. 2. **Hash match** (confidence 0.95) — MediaItem.Hash (file content hash) 3. **audioMd5 match** (confidence 0.95) — decoded audio hash (same audio, different container) 4. **metadataKey match** (confidence 0.50) — normalized artist|album|title|duration This enables cross-machine / cross-ingest lookups: a file scanned on Machine B under a different drive or user path is still found via PathTail (tier 1b) for the majority case, falling back to audioMd5 or metadataKey when the tail differs too. ### REST vs RPC - Use REST for client apps, clean URLs, resource-oriented design - Use RPC (`POST /rpc/{methodName}`) for direct MusicBee API access, automation scripts ### Real-time Updates - Use REST for commands (play, pause) and queries (get queue, search) - Use WebSocket for real-time UI updates, progress bars, visualizations ### AutoQ Availability Shuffle, banlist, and influence endpoints require TrueShuffle or AutoQ to be enabled: - Returns `503 SERVICE_UNAVAILABLE` if TrueShuffle/AutoQ not enabled - Check with `GET /shuffle/status` ## Anatomy of a Player Common UI regions and the endpoints that power them: ### Now Playing Panel Current track display - artwork, title, artist, album - `GET /nowplaying` - Track metadata - `GET /nowplaying/artwork` - Album art - `GET /nowplaying/lyrics` - Lyrics (if available) - WebSocket `TrackChanged` event - Live updates on track change ### Transport Controls Play, pause, stop, next, previous buttons - `POST /player/play` - `POST /player/pause` - `POST /player/playpause` - Toggle - `POST /player/stop` - `POST /player/next` - `POST /player/previous` - WebSocket `PlayStateChanged` event - Button state updates ### Progress / Seek Bar Position slider, elapsed time, total duration - `GET /nowplaying/position` - Current position - `PUT /player/position` - Seek to position - WebSocket `PositionChanged` event - Live position updates (see seek bar sample code) ### Volume Control Volume slider, mute button - `GET /player/volume` - Current volume (0-100) - `PUT /player/volume` - Set volume - `GET /player/mute` - Mute state - `PUT /player/mute` - Toggle mute - WebSocket `VolumeChanged` event - Live updates ### Mode Controls Shuffle toggle, repeat cycle, AutoDJ - `GET /player/shuffle` - Shuffle state (true/false) - `PUT /player/shuffle` - Set shuffle (body: {"shuffle":true/false}) - `GET /player/autodj` - AutoDJ state - `POST /player/autodj/start` - Start AutoDJ - `POST /player/autodj/stop` - Stop AutoDJ - `GET /player/repeat` - Repeat mode (off/all/one) - `PUT /player/repeat` - Set repeat (body: {"repeat":"off|all|one"}) - WebSocket `ShuffleChanged` ({enabled:bool}), `RepeatChanged` events ### Dashboard Pages - `GET /dashboard` - Main dashboard page (HTML). Redirects to default page if configured. - `GET /dashboard/theme` - Switch theme mode (query: `?mode=1|2`). Saves to settings and broadcasts ThemeChanged WebSocket event. ### Dashboard Transport (SSR form POSTs) - `POST /dashboard/play` - Play - `POST /dashboard/pause` - Pause - `POST /dashboard/stop` - Stop - `POST /dashboard/next` - Next track - `POST /dashboard/previous` - Previous track (alias: `/dashboard/prev`) - `POST /dashboard/shuffle` - Toggle shuffle - `POST /dashboard/shuffle-off` - Shuffle off - `POST /dashboard/autodj` - Toggle AutoDJ - `POST /dashboard/repeat` - Cycle repeat mode ### Dashboard Volume - `POST /dashboard/volup` - Volume +5% - `POST /dashboard/voldown` - Volume -5% - `POST /dashboard/volup1` - Volume +1% (fine) - `POST /dashboard/voldown1` - Volume -1% (fine) - `POST /dashboard/mute` - Toggle mute - Keyboard: / (search), Arrow Up/Down (1%), Shift+Arrow (5%), M (mute) ### Rating / Love / Ban Star rating (1-5), bomb (0=don't play), love/heart, ban from shuffle - `POST /dashboard/rate/0` - Toggle bomb (don't play). Click on, click off - `POST /dashboard/rate/1-5` - Toggle star rating. Click to set, click same to clear - `POST /dashboard/love` - Toggle love tag - `POST /dashboard/setban` - Set ban flag without skipping - WebSocket `MetadataChanged` event for changes ### AutoQ Mood & Reactions Mood-based track selection and user reactions - `POST /dashboard/mood` (form: mood=Energetic) - Set mood channel - `POST /dashboard/react/fire|heart|like|dislike` - Submit reaction - `POST /dashboard/ban` - Ban current track (skip + add to banlist) - `POST /dashboard/refresh-queue` - Refresh vibe list and queue tracks - `POST /dashboard/influence/artist|genre/up|down` - Thumbs up/down for artist or genre - Fire reaction triggers immediate queue refresh ### Queue / Up Next List of upcoming tracks - `GET /queue` - Full queue with pagination - `GET /queue/current` - Current track index - `POST /queue/add` - Add track (next or last) - `DELETE /queue/{index}` - Remove track - `POST /queue/move` - Reorder - WebSocket `QueueChanged` event - Queue changes ### Library Browser Browse by artist, album, genre - `GET /library/artists` - Artist list - `GET /library/albums` - Album list (filter by artist) - `GET /library/albums/detailed` - Albums with firstTrackUrl (for artwork, avoids N+1) - `GET /library/albums/unheard` - Unplayed albums - `GET /library/albums/with-pdf` - Albums with PDF booklets - `GET /library/genres` - Genre list - `GET /library/inbox` - Inbox files (shown in browse only if non-empty) - `GET /library/audiobooks` - Audiobook files (shown in browse only if non-empty) - `GET /library/videos` - Video files (shown in browse only if non-empty) - `GET /library/files` - Track list (filter by artist/album/genre) - `GET /library/search?q=` - Search (strict word-boundary by default; `&substring=true` for loose matching) ### Playlists Panel List and manage playlists - `GET /playlists` - All playlists - `GET /playlists/{url}/files` - Tracks in playlist - `POST /playlists/{url}/play` - Play playlist - `POST /playlists` - Create new - `POST /dashboard/playlist` - Play or queue a playlist from the dashboard (form: playlistUrl=...) ### Status Bar Connection status, server info - `GET /ping` - Health check - `GET /system/version` - Version info - WebSocket connection state ## Sample Code (player.html patterns) ### Fetch Now Playing ```javascript async function getNowPlaying() { const res = await fetch('/nowplaying'); const json = await res.json(); if (json.success) { document.getElementById('title').textContent = json.data.title; document.getElementById('artist').textContent = json.data.artist; document.getElementById('album').textContent = json.data.album; } } ``` ### Display Artwork ```javascript document.getElementById('artwork').src = '/nowplaying/artwork?' + Date.now(); ``` ### Player Controls ```javascript async function play() { await fetch('/player/play', {method:'POST'}); } async function pause() { await fetch('/player/pause', {method:'POST'}); } async function next() { await fetch('/player/next', {method:'POST'}); } async function prev() { await fetch('/player/previous', {method:'POST'}); } async function setVolume(v) { await fetch('/player/volume', { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({volume:v}) }); } ``` ### WebSocket Live Updates ```javascript const ws = new WebSocket('ws://' + location.host + '/ws'); ws.onmessage = (e) => { const msg = JSON.parse(e.data); switch(msg.event) { case 'TrackChanged': document.getElementById('title').textContent = msg.data.title; document.getElementById('artwork').src = '/nowplaying/artwork?' + Date.now(); break; case 'PlayStateChanged': updatePlayButton(msg.data.state); break; case 'PositionChanged': updateProgress(msg.data.position, msg.data.duration); break; case 'VolumeChanged': updateVolumeSlider(msg.data.volume); break; } }; ``` ### Browse Library ```javascript async function getArtists() { const res = await fetch('/library/artists?limit=100'); const json = await res.json(); return json.data; // array of artist names } async function getAlbumsByArtist(artist) { const res = await fetch('/library/albums?artist=' + encodeURIComponent(artist)); const json = await res.json(); return json.data; // array of album objects } async function searchLibrary(query) { const res = await fetch('/library/search?q=' + encodeURIComponent(query)); const json = await res.json(); return json.data; // array of track objects } ``` ### Queue Management ```javascript async function addToQueue(url, position='last') { await fetch('/queue/add', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({url:url, position:position}) }); } async function playNow(url) { await fetch('/queue/playnow', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({url:url}) }); } ``` ### Playlists - File vs URL Paths ```javascript // GOTCHA: Playlist URLs from API are file paths, must be encoded for use in URLs // Get all playlists async function getPlaylists() { const res = await fetch('/playlists'); const json = await res.json(); return json.data; // [{name:'My Playlist', url:'C:\\Users\\...\\My Playlist.m3u'}, ...] } // Get tracks in a playlist - MUST encode the path async function getPlaylistTracks(playlistUrl) { // playlistUrl is a Windows path like 'C:\Users\...\My Playlist.m3u' const encoded = encodeURIComponent(playlistUrl); const res = await fetch('/playlists/' + encoded + '/files'); const json = await res.json(); return json.data; // array of file paths } // Play a playlist async function playPlaylist(playlistUrl) { const encoded = encodeURIComponent(playlistUrl); await fetch('/playlists/' + encoded + '/play', {method:'POST'}); } // Add tracks to playlist - tracks are also file paths async function addToPlaylist(playlistUrl, trackUrls) { const encoded = encodeURIComponent(playlistUrl); await fetch('/playlists/' + encoded + '/files', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({urls:trackUrls}) // trackUrls are file paths, not encoded here }); } // Create a playlist - returns the new playlist's file path async function createPlaylist(name) { const res = await fetch('/playlists', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:name}) }); const json = await res.json(); return json.data.url; // file path to new playlist } // Key insight: // - In URL path: encode with encodeURIComponent() // - In JSON body: use raw file paths (no encoding) ``` ### Love/Unlove Toggle (RatingLove) `/dashboard/love` toggles the RatingLove tag on the now-playing track. No request body. ```javascript let loved = false; async function toggleLove() { await fetch('/dashboard/love', { method: 'POST', headers: { 'Content-Length': '0' } }); loved = !loved; document.getElementById('love-btn').classList.toggle('loved', loved); } // Sync from now-playing on connect / track change ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.event === 'TrackChanged') { fetch('/nowplaying').then(r => r.json()).then(j => { loved = j.success && (j.data.ratingLove === 'L' || j.data.ratingLove === true); document.getElementById('love-btn').classList.toggle('loved', loved); }); } }; ``` ### 3-Way Shuffle Toggle (off → shuffle → autodj → off) Shuffle is a boolean. AutoDJ is a separate mode with its own endpoints. ```javascript let shuffleEnabled = false; let autoDjEnabled = false; // Determine current mode from player status async function loadShuffleState() { const res = await fetch('/player/status'); const json = await res.json(); shuffleEnabled = json.data.shuffle; autoDjEnabled = json.data.autoDj; updateShuffleButton(); } // Cycle: off → shuffle → autodj → off async function cycleShuffle() { if (!shuffleEnabled && !autoDjEnabled) { // off → shuffle await fetch('/player/shuffle', { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({shuffle:true}) }); } else if (shuffleEnabled && !autoDjEnabled) { // shuffle → autodj await fetch('/player/autodj/start', {method:'POST'}); } else { // autodj → off await fetch('/player/autodj/stop', {method:'POST'}); await fetch('/player/shuffle', { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({shuffle:false}) }); } // Don't update local state - wait for WebSocket confirmation } function updateShuffleButton() { const btn = document.getElementById('shuffle-btn'); const mode = autoDjEnabled ? 'autodj' : shuffleEnabled ? 'shuffle' : 'off'; btn.className = 'shuffle-' + mode; btn.title = mode === 'off' ? 'Shuffle Off' : mode === 'shuffle' ? 'Shuffle' : 'AutoDJ'; } ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.event === 'ShuffleChanged') { shuffleEnabled = msg.data.enabled; updateShuffleButton(); } }; ``` ### Seek Bar with Position Updates ```javascript let duration = 0; let position = 0; let isSeeking = false; // Prevent WebSocket updates while user is dragging const seekBar = document.getElementById('seek-bar'); // User starts dragging seekBar.addEventListener('mousedown', () => { isSeeking = true; }); seekBar.addEventListener('touchstart', () => { isSeeking = true; }); // User releases - send seek command seekBar.addEventListener('mouseup', doSeek); seekBar.addEventListener('touchend', doSeek); async function doSeek() { isSeeking = false; const newPos = Math.round((seekBar.value / 100) * duration); await fetch('/player/position', { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({position:newPos}) }); } // WebSocket position updates - ignore while seeking ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.event === 'PositionChanged' && !isSeeking) { position = msg.data.position; duration = msg.data.duration; seekBar.value = duration > 0 ? (position / duration) * 100 : 0; document.getElementById('time-current').textContent = formatTime(position); document.getElementById('time-total').textContent = formatTime(duration); } if (msg.event === 'TrackChanged') { duration = msg.data.duration || 0; position = 0; seekBar.value = 0; } }; function formatTime(ms) { const s = Math.floor(ms / 1000); const m = Math.floor(s / 60); return m + ':' + String(s % 60).padStart(2, '0'); } ``` ## Further Reading These docs are fetchable from a running MBXHub instance. Read them for complete details. ### GET /docs - Full API Reference (alias: `/api`) Complete documentation for all 175+ endpoints with: - Request/response examples for every endpoint - Query parameters and body schemas - Error codes and responses - Integration notes and best practices **Read when:** You need exact parameter names, response shapes, or edge cases ### GET /aria - ARiA Automation Guide Remote input simulation and automation: - DuckyScript syntax for keyboard macros - Preset configuration (JSON format) - Wake-on-LAN for sleeping PCs - MusicBee hotkey integration - Security considerations **Read when:** Building automation, macros, or remote wake features ### GET /changelog - Version History Release notes for all versions: - New features and endpoints - Breaking changes - Bug fixes **Read when:** Checking what's new or if an endpoint exists in current version ### GET /pages/player.html - Working Example Full source code of the included web player with all JS inline: - 3-column responsive layout (browse, now playing, queue) - All fetch() calls, WebSocket handling, state management - Browse iframe with theme sync via postMessage - Drag-drop queue reorder with touch support **Read when:** You want to see how something is implemented or need working code to copy/adapt ## Website - https://mbxhub.com - Latest releases and downloads - https://mbxhub.com/features.html - Feature overview with screenshots - https://mbxhub.com/api.html - API docs (same as /docs)