The bridge to everything else
v0.5.4.6It's as simple as:
# Get current track info (just open in browser)
http://localhost:8080/nowplaying
# Get album artwork (returns image)
http://localhost:8080/nowplaying/artwork
# Browse all albums in library
http://localhost:8080/library/albums
# Skip to next track
curl -X POST http://localhost:8080/player/next
Try it now: /nowplaying • /nowplaying/artwork • /library/albums
Web-based remote control with integrated library search. All actions use POST-Redirect-GET pattern. Supports keyboard shortcuts: / (search), Space (play/pause), ←/→ (prev/next), ↑/↓ (volume ±1%), Shift+↑/↓ (volume ±5%), M (mute).
Full web dashboard with library search, now playing, controls, volume, ratings, shuffle/autodj/repeat toggles. Panel visibility, order, and collapsible grouping are configurable via dashboardLayout in settings.
Search: client-side fuzzy album/artist matching + server-side track search. Albums containing matching tracks are boosted (e.g. “wonderwall” surfaces its album). Inline track expansion on album results. Explore button seeds explore.html?album=&artist=.
volup/voldown - Adjust volume by 5%.
volup1/voldown1 - Adjust volume by 1% (fine control).
shuffle=enable, shuffle-off=disable both, autodj=start AutoDJ, repeat=cycle mode
love - Toggle love tag on current track.
rate/0 - Toggle bomb (don't play). Click on → click off.
rate/0.5-5 - Toggle star rating; 0.5 increments accepted (e.g. 2.5). Click to set, click same to clear.
ban - Ban track from shuffle, skip to next (undo: skip back within 10s).
setban - Set ban flag without skipping.
mood - Set AutoQ mood channel (form: mood=Energetic).
react/fire|heart|like|dislike - Submit reaction. Fire triggers queue refresh.
refresh-queue - Refresh vibe list and queue tracks.
influence/artist|genre/up|down - Thumbs up/down for artist or genre.
Play or queue a playlist (form: playlistUrl=...&action=now|next|last).
Dashboard layout, display toggles, and feature kill-switches are all configured in mbxhub.json. Full field list with defaults, types, and descriptions: GET /system/settings/schema or the config reference. Live feature state (except proxy) is exposed via GET /system/features. Section IDs for dashboardLayout.order and .hidden: status, search, nowplaying, rating, controls, volume, charms, mood, playlists, toggles, footer.
Now Playing Styles (nowPlayingStyle, default: "full"):
| Style | Description |
|---|---|
full | Standard layout with full-size artwork and metadata |
horizontal | Side-by-side artwork and metadata |
noart | No artwork, text-only display |
split | Two-column grid (art left, metadata right). Column ratio configurable via localStorage mbxh_split_ratio (default 55, range 40–70). Falls back to stacked below 480px |
immersive | Full-bleed album art with gradient overlay and blurred letterbox fill for non-square art. Metadata fades on hover/touch, fades out after dashboardLayout.immersiveFadeDelay (0 = always visible). Set dashboardLayout.immersiveAlwaysShowProgress: true to pin the seek bar at full height regardless of the chrome fade. |
Client-side override: localStorage key mbxh_np_style overrides the server default per-device. Header button cycles through all styles. All styles have zoom-level overrides (67%, 50%, 27%). The compact style was removed in v0.5.2.3 — existing configs gracefully fall back to full.
MBXHub can serve custom HTML pages from a configurable directory. This enables building custom web UIs that use the REST API.
/pages/ with correct Content-Type headers.
Default pages are extracted on first run and can be customized without losing changes on updates.
Set pagesPath in mbxhub.json to customize the pages directory:
{
"pagesPath": "C:\\MyCustomPages"
}
Default location: %APPDATA%\MusicBee\MBXHub\pages\
Set defaultPage to change the root URL redirect:
{
"defaultPage": "/pages/player.html"
}
Options: /dashboard (default), /pages/player.html, /pages/play.html, /pages/partymode/, /pages/nowplaying.html, or any custom page.
Kiosk Mode: Lock the display to a single page/app. All navigation redirects to the default page.
{
"defaultPage": "/pages/partymode/",
"kioskMode": true
}
Multi-page apps work: sub-paths are allowed (e.g., /pages/partymode/ allows /pages/partymode/guest.html). API calls and resources still work. Only editable in mbxhub.json (not exposed via API).
Serves index.html from the pages directory
Serves any file from the pages directory (HTML, CSS, JS, images, fonts)
/pages/index.html - Landing page listing available views/pages/player.html - Legacy (kept for bookmarks and default-page users; receives no new features — use /pages/play.html). Full-featured desktop player with:
/pages/nowplaying.html - Focused now-playing view with artwork, lyrics, and queue. Auto-stacks below 800px with accordion sections (Now Playing, Queue, Lyrics) — expanding one collapses the others/pages/browse.html - Library browser with 8 responsive tabs, drilldown navigation, fuzzy search with auto-search fallback (triggers server search when no inline matches), album art grid, video direct play, batch queue, and playlist picker/pages/config.html - Settings and configuration for dashboard layout and AutoQ parameters/pages/autoq.html - AutoQ Tuning Console with mixer-style sliders for scoring weights and normalization ranges/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/pages/explore.html - Album art explorer: browse albums as artwork with source filters, search, sort, image gallery, PDF booklets, play/queue. Accepts ?album=&artist= for seeding. Artist discography grid in expanded view. Expanded-view hero has paired close (×) and dashboard home (⌂) buttons — close stays on explore, home returns to /dashboard (home hidden in iframe-mode)/pages/play.html - Use MusicBee from a browser. Laid out like the MusicBee AMOLED skin: artist picker (left) with infinite-scroll + jump-on-letter typeahead, pluggable middle pane (Albums / Library / Now-Playing), upcoming-queue + now-playing card (right), full-width transport footer.
/influences/currentmbxhub.log via /system/client-log (no console-only logging)/pages/components/dashboard.css — Dashboard stylesheet (~2,900 lines). Served as an external linked resource by /dashboard. Theme variables are injected separately in an inline <style> block ahead of this link; this file holds the bulk of dashboard styling (layout, transport, charm bar, command palette, theme designer, party banner, search results). Linked with ?v={version} for release-driven cache invalidation./pages/components/dashboard.js — Dashboard client script (~2,800 lines). WebSocket lifecycle, transport handlers, theme designer, charm bar, partial-section reload, Cmd+K wiring. Loaded with defer by /dashboard after an inline <script> that defines the per-request _themeData JSON. Same ?v={version} cache-busting pattern./pages/components/cmdk.html — v0.5.3.0 Cmd+K command palette. Self-mounting <style> + <dialog> + <script> fragment. XSS-safe (textContent only, no untrusted innerHTML). Federated search via /search, recents via /search/history with localStorage fallback. Actions are POSTs (Play/Pause/Skip/Volume, Start AutoQ radio) or navigations (Charms / Settings / Mixer / Player / Browse / Explore / ARiA). Settings nested under dashboardLayout.commandPalette.{enabled, openShortcut, showChip, recentLimit, bucketLimit, actions, enabledOnPages}./pages/components/cmdk-bootstrap.js — v0.5.3.0 palette loader. Drop-in <script> include — loads search-shared.js then injects cmdk.html via DOMParser so embedded scripts execute. Self-mounting; degrades gracefully when search-shared.js is unreachable. Wired into dashboard, play.html, explore.html, nowplaying.html, and browse.html./pages/search-shared.js — Search lifecycle helper. Provides MBXSearch.attachSearch for debounced typeahead with abort-on-keystroke (exposed as .abort() on the returned handle for v0.5.3.0+ callers that need to cancel in-flight fetches before navigation). Required by cmdk; usable standalone./pages/components/shared.css — v0.5.3.0 shared frontend core. Cross-page styles consolidated from per-page duplication: CSS reset + box-sizing, prefers-reduced-motion rules, focus / focus-visible defaults, dual theme palettes (default + theme-quiet, light + dark), scrollbar styling, .album-art-placeholder, .hl highlight class, .empty-state. Linked first in each shell page's <head> so per-page styles can override./pages/components/shared.js — v0.5.3.0 shared frontend core. Cross-page helpers under the MBXShared global namespace: setPageName / getPageName, clientLog (batched + sendBeacon-on-pagehide) + flushClientLog, esc (HTML-encode), DIACRITICS (char map ported from MusicBee's character map — union of the browse / explore / player maps, 1:1 char invariant preserved), normalize (iter-based: DIACRITICS lookup → lowercase → strip apostrophes → collapse non-alphanumerics to single spaces → trim; NFD form removed in the Option C foundation), findHighlightSpans(text, query) (returns Array<{start, end}> half-open ranges in ORIGINAL-text index space — walks normalized text, finds full-phrase + per-word occurrences, leaves HTML rendering to the caller so pages can wrap spans their own way), fmtDuration (ms → m:ss or h:mm:ss), connectWebSocket (subscribe + JSON parse + dispatch-by-event + auto-reconnect with capped backoff), urlHashOf(url) (22-char base64url SHA1-truncated hash of a file URL — used for /library/file/{urlHash}/* routes and ?trackUrl= deep links; byte-equivalent on secure and non-secure LAN-HTTP contexts). Loaded with defer by every shell page./manifest.webmanifest — v0.5.3.2 PWA manifest. Returns application/manifest+json. Both name and short_name are templated with the host's advertised name — the discoveryName field in mbxhub.json, set via Plugin Settings → “Advertise on local network → Name”. The advertised name is independent from the machine's hostname: a host whose Windows computer name is living-room-pc but whose advertised name is “prod” serves name: "MBXHub - prod" (browser install dialogs / Manage apps UI) and short_name: "MBXHub - prod" (OS desktop / home-screen / app-drawer labels) — “living-room-pc” never appears in the install UI. If discoveryName is empty, the manifest falls back to the Windows computer name, and ultimately to the literal "host" if even that is unavailable. Trade-off: changing the advertised name updates both fields on the next manifest fetch — already-installed devices see the new label after one reinstall or manifest refresh. Standalone display, theme color matches the dashboard surface, icons array references /icons/icon-{192,512}.png plus maskable variants. Served with Cache-Control: no-cache (Chromium's installed-PWA “update on reload” path otherwise heuristically caches the manifest indefinitely). Note: PWA install itself is gated by browser policy to HTTPS or localhost; over plain LAN HTTP the manifest still drives iOS “Add to Home Screen” and Android home-screen pins (no service worker required for those), but the rich Chrome/Edge “Install as app” surface is unavailable./pages/history.html — v0.5.3.2 Recently-played viewer. Day-range chips (24h / 7d / 30d / etc.) sourced from GET /library/recent?days=N. Track list shows title / artist / album / albumArtist plus playCount + skipCount columns; artist and album cells are clickable links into /pages/browse.html. Per-row P (Play Now) and L (add to MusicBee queue) buttons. Page-2 prefetch as the user scrolls. Backed by the /library/recent cache (10-min TTL, invalidated on TrackChanged). Footer link disabled by default./pages/hubs.html — v0.5.3.2 Hub-switcher page. Renders the “Hub neighbors” list — other MBXHub instances this browser has reached recently, read from localStorage['mbxhub-hub-neighbors'] — plus a free-text host:port input fallback. Pure DOM, no innerHTML, no remote calls. Footer link disabled by default./icons/{name} — v0.5.3.0 PWA icon set. Resolves the embedded MBXHub.Resources.icons.{name} and serves with image/png. Standard set: icon-192.png, icon-512.png, icon-maskable-192.png, icon-maskable-512.png, apple-touch-icon-180.png.To customize pages:
player.html or create new HTML filesMBXHub serves /llms.txt - an AI-friendly API reference. Use it with Claude or any AI to generate custom pages:
/nowplaying, /player/play) that work on any MBXHub instance%APPDATA%\MusicBee\MBXHub\pages\http://localhost:8080/pages/partyon.htmlThe generated code uses relative URLs, so it works on your local MBXHub without modification.
MBXHub advertises itself on the local network via three protocols so clients can find it automatically.
DnsServiceRegister API.
Requires Windows 10 1809+; gracefully skipped on older systems. Registered as MBXHub (Name)._http._tcp.local with TXT records for path and version.
UPnP device description XML. Contains device info, service URLs, and presentation URL.
WS-Discovery metadata exchange endpoint. Windows sends a SOAP GetMetadata request after discovering MBXHub via UDP Probe; response includes PresentationUrl pointing to /dashboard.
| Field | Description |
|---|---|
| deviceType | urn:halrad-com:device:MBXHub:1 |
| friendlyName | MBXHub instance identifier |
| presentationURL | Dashboard URL (/dashboard) |
| controlURL | REST API base (/api) |
| eventSubURL | WebSocket endpoint (/ws) |
| Port | Protocol | Owner | Purpose | When needed |
|---|---|---|---|---|
| 8080 | TCP | Plugin | REST API + WebSocket (restPort) | Always — the plugin's main HTTP listener |
| 8081 | TCP | Shell | SMTC control routes /meta/smtc/* (smtc.port, convention is REST port + 1) | Only when MBXHub.exe is running. Open it for remote SMTC retargeting from the dashboard, or for Bluetooth/lock-screen control on a different machine. Skip it for plugin-only / NAS / headless installs. |
| 1900 | UDP | Plugin | SSDP (UPnP discovery) | For Windows Network folder + SSDP browsers |
| 3702 | UDP | Plugin | WS-Discovery (Windows Network folder) | For Windows Explorer Network integration |
| 5353 | UDP | Plugin | mDNS/DNS-SD (device discovery via raw UDP multicast) | For Bonjour-style zero-conf discovery (Win10 1809+) |
Plugin-managed (REST port + Shell SMTC port + UDP discovery) — one rule covers everything the plugin can see. Use the Settings → Firewall panel or CLI:
MBXHub.exe firewall add --name MBXHub --tcp 8080,8081 --udp 1900,3702,5353 --urlacl 8080,8081
Shell-only (SMTC port, added by Shell itself) — MBXHub.exe --install writes a MBXHub-Shell rule and URL ACL for smtc.port automatically; --uninstall removes it. You only need to run firewall commands manually if you skip --install.
Override the Shell SMTC port by editing smtc.port in mbxhub-shell.json. The plugin's firewall helper auto-adopts restPort + 1 as the Shell port; if you change smtc.port from that convention, open the new value manually.
discoveryEnabled (default: true), discoveryName (default: empty = machine name).
The discovery name is used as the friendly name across all protocols and in GET /system/version.
HTML status dashboard with live stats: system info (version, uptime, host, modules), library counts (tracks, albums, artists, genres, playlists, podcasts), AutoQ state (status, vibe list, mood cache, auto scan), and feature flags. All data fetched client-side from existing API endpoints.
Returns system status and enabled modules (JSON)
Diagnostic surface — a live “Geiger counter” for steady-state activity. Off by default; gated on diagnostics.diagEndpointEnabled = true in mbxhub.json. Returns 404 when disabled, with explanatory body.
/diag serves a self-contained HTML page that polls /diag/perf at 1 Hz from the browser and renders inline-SVG sparklines for: process CPU %, network bytes in/out, working-set / private bytes, thread count, handle count, GC gen0/1/2 collection rate. Last 120 samples (~2 min) held client-side. Server is stateless — no background timer, no ring buffer, no allocation while the page is closed.
/diag/perf returns an instant snapshot of public process counters via System.Diagnostics.Process, System.Net.NetworkInformation, and GC.CollectionCount. Fields: ts, processorMs, workingSetMB, privateMB, threadCount, handleCount, gen0/gen1/gen2, bytesIn, bytesOut, logicalProcessors. Cumulative counters; the page diffs them client-side to derive per-second rates.
/diag/mbxhub returns a per-MBXHub instrumentation snapshot — counters specific to this plugin (request rates, broadcaster fan-out, lock waits, etc.) rather than the OS-level metrics in /diag/perf. The /diag page renders this as a second tile row. Same diagEndpointEnabled gate.
/diag/snapshot (v0.5.2.6+) takes a JSON body containing the page’s client-side ring (samples + derived rates + small header) and appends a formatted text block to diag-snapshots.log next to mbxhub.log. Each snapshot is delimited by “=” rules with a per-metric min/avg/max/latest table and the raw samples JSON below for forensic re-processing. Triggered by the Snapshot button on the /diag page; same diagEndpointEnabled gate. Response: { success, path, bytes, at }.
/diag/search (v0.5.3.0) returns a rolling p50/p90/p95/p99 summary over the last 256 search calls (in-memory ring, ~28 KB, server-wide singleton). Summary is segregated by endpoint: library (calls into /library/search), federated (calls into /search), and combined. Each block carries totalMs {p50,p90,p95,p99,max,mean}, candidates {p50,p95,max}, and mbCalls {p50,p95,max}. The recent array (default 50, override with ?recent=N, capped at ring capacity) lists the newest entries with per-stage timings (mb / cue / am / pf / sort / pg) and per-family MB-call counts (qf / ql / qlr / gt / gts / gp / gb / pl). POST /diag/search/reset clears the ring (operator-triggered flush; empty body, send Content-Length: 0). Same diagEndpointEnabled gate. Returns 403 FORBIDDEN when ApiReadOnlyMode is set — kiosk deployments can’t have their perf history wiped by remote clients.
// Enable in mbxhub.json:
{
"diagnostics": {
"diagEndpointEnabled": true
}
}
// Then visit:
// http://host:8080/diag → live page
// http://host:8080/diag/perf → JSON snapshot
Returns API version information. Includes host field with the configured discovery name (or machine name if not set).
Returns this node's capabilities for cross-channel SSDP discovery. Used by Shell and other MBXHub nodes to identify what this instance offers.
// Response:
{
"node": "plugin",
"version": "0.5.4.6",
"capabilities": ["rest-api", "websocket", "player-control", "autoq", "discovery"],
"endpoints": {
"rest": "http://host:8080",
"ws": "ws://host:8080/ws"
}
}
Health check endpoint (alias: /system/ping)
Returns enabled feature flags: banlist, ratings, loved, reactions, streaming, playReporting (false when apiDisablePlayReporting or apiDisablePlayCountUpdates is on — clients watching this flag stop sending play reports under either), partymode, autoq, diag (v0.5.3.4 — mirrors diagnostics.diagEndpointEnabled). Clients use these to show/hide UI elements.
v0.5.3.4. Returns the friendly display-name MB has configured for each of the 48 tags exposed by browse.customSorts (Custom1..Custom16, Virtual1..Virtual25, Year, OriginalYear, SortAlbum, SortAlbumArtist, SortArtist, SortTitle, SortComposer). Server reads Setting_GetFieldName for each; only entries whose user-set name differs from the raw enum are returned. Used by settings.html's customSorts tag-picker to render AutoQ Moods (Custom1) instead of just Custom1.
// GET /system/custom-tag-names
{
"success": true,
"data": {
"names": {
"Custom1": "AutoQ Moods",
"Custom2": "Mood Rating",
"Virtual1": "Energy"
}
}
}
Get or update hub configuration (ports, enabled modules, log verbosity, library behavior). restPort and restEnabled changes require localhost (403 for remote). All changes blocked for remote clients during party mode.
Beyond the network fields (restPort, restEnabled, wsEnabled, defaultPage), GET and PUT also surface (MBRC was cut; mbrcPort / mbrcEnabled no longer exist):
{
"logLevel": "Info", // trace/debug/info/warn/error, case-insensitive; propagates to Shell
"library": { "disableStrictSearch": false }, // v0.5.2.4+: disables phrase-per-field strict search
"search": { "live": { "minQueryLength": 2 } } // v0.5.3.0: server-side minimum-length backstop for /search and /library/search
}
logLevel is validated against the same vocabulary as HubLogger.ParseLevel/ShellLog.ParseLevel; invalid values are silently ignored (same soft-skip pattern used for restPort range checks). library.disableStrictSearch takes a nested object — the parent object is created if missing. search.live.minQueryLength defaults to 2; queries below this return 400 QUERY_TOO_SHORT. None of these require localhost; all are UX-level settings safe to change remotely.
Returns schema for all configurable settings. Each entry includes:
| Field | Description |
|---|---|
key | Dotted key path (e.g. autoQ.batchSize) |
type | bool, int, double, string, enum |
category | Grouping: General, AutoQ, Scoring, Dashboard, API, etc. |
tier | Standard, Advanced, or Expert |
description | Human-readable explanation |
default | Default value |
current | Current live value |
min, max, step | Range constraints (numeric types only) |
options | Valid values (enum types only) |
requiresRestart | Whether a restart is needed for the change to take effect |
Security: blocked when disableRemoteConfig is true (403) or during party mode (403). The /pages/settings.html page consumes this endpoint.
Update configurable settings via dotted key paths. Only properties decorated with [ConfigSetting] can be modified.
Body: JSON object with dotted keys:
{"autoQ.batchSize": 10, "apiReadOnlyMode": true}
Security: blocked by apiReadOnlyMode (403), disableRemoteConfig (403), and party mode (403). POST is an alias for PUT.
Get or set the default redirect page (body: {"defaultPage":"/pages/player.html"})
Get or update the unified theme configuration. Two configurable mode slots (mode1, mode2) each with 13 HSL fields: accentHue (0–360), accentSaturation (0–100), accentLightness (0–100), bgHue (0–360), bgSaturation (0–100), bgLightness (0–100), surfaceHue (0–360), surfaceSaturation (0–100), surfaceLightness (0–100), textHue (0–360), textSaturation (0–100), textLightness (0–100), intensity (0–100, saturation multiplier: 0 = grayscale, 100 = full color). Partial updates supported — only include the fields you want to change.
// GET response:
{
"activeMode": 1,
"mode1": {
"accentHue": 197, "accentSaturation": 80, "accentLightness": 55,
"bgHue": 203, "bgSaturation": 30, "bgLightness": 94,
"surfaceHue": 203, "surfaceSaturation": 30, "surfaceLightness": 96,
"textHue": 203, "textSaturation": 50, "textLightness": 13,
"intensity": 100
},
"mode2": {
"accentHue": 197, "accentSaturation": 80, "accentLightness": 55,
"bgHue": 203, "bgSaturation": 30, "bgLightness": 7,
"surfaceHue": 203, "surfaceSaturation": 30, "surfaceLightness": 10,
"textHue": 203, "textSaturation": 50, "textLightness": 90,
"intensity": 100
},
"active": {
"accentHue": 197, "accentSaturation": 80, "accentLightness": 55,
"bgHue": 203, "bgSaturation": 30, "bgLightness": 94,
"surfaceHue": 203, "surfaceSaturation": 30, "surfaceLightness": 96,
"textHue": 203, "textSaturation": 50, "textLightness": 13,
"intensity": 100
},
"disablePinchZoomLock": false
}
// PUT examples:
{"activeMode": 2} // Switch to mode 2
{"mode1": {"accentHue": 180}} // Update just accent hue on mode 1
{"mode1": {"bgLightness": 10, "textLightness": 90}} // Make mode 1 dark
{"mode1": {"intensity": 0}} // Desaturate mode 1 to grayscale
PUT broadcasts a ThemeChanged WebSocket event and sets the mbxh_theme cookie for dashboard SSR.
Process metrics for the MusicBee host process. Used by MBXHVAL for remote monitoring.
// Response:
{
"success": true,
"data": {
"process": {
"name": "MusicBee",
"cpuPercent": 2.15,
"memoryMB": 185.3,
"privateMemoryMB": 210.5,
"threadCount": 42,
"handleCount": 1250
},
"gc": {
"gen0": 150,
"gen1": 30,
"gen2": 5,
"totalMemoryMB": 45.2
},
"timestamp": "2026-03-10T12:00:00.0000000Z"
}
}
Server start time and uptime. Used by dashboard to detect restarts.
// Response:
{
"success": true,
"data": {
"startedAt": "2026-03-10T20:30:00.0000000Z",
"uptimeSeconds": 3600,
"uptime": "1h 0m"
}
}
Generate QR code PNG image for the MBXHub base URL. Optional ?url= for custom target.
Returns the client's IP address as seen by the server. Used by the SMTC Link Charm to discover the client's local Shell when accessing a remote dashboard.
Push a named event into the WebSocket broadcaster from sub-process components (Shell, scripts, automations). The request body is forwarded as the event payload; dashboards subscribed to {name} receive it. Used to fan out signals that originate outside the plugin process.
Force re-extract all embedded resources (pages and charms) to disk. Overwrites existing files.
// Response:
{
"success": true,
"data": {
"pagesUpdated": 8,
"charmsUpdated": 3,
"userModified": []
}
}
Accepts browser-side log events and writes them to mbxhub.log. Use this instead of console.log for operational telemetry from dashboard pages so problems in guest browsers land in the server-side log where the operator can see them. Accepts either a single event or a batch via events[]. Silent on empty body (returns {success:true, data:{accepted:0}}); accepted counts events that passed validation and reached the log. Invalid JSON returns 400 INVALID_REQUEST (not INVALID_JSON — this endpoint predates the topology handler’s specific code). Access: ActionCategory.System — admin-gated. PartyMode guests are rejected (they lack System) so guest-sourced log entries can’t mix into operator telemetry.
// Single event:
POST /system/client-log
{ "level": "warn", "msg": "Retry #3 on /queue/add" }
// With page context (prefixes the log line as "[page] msg"):
POST /system/client-log
{ "level": "info", "msg": "Charm bar rendered", "page": "dashboard" }
// Batch:
POST /system/client-log
{ "events": [
{ "level": "info", "msg": "Charm bar rendered" },
{ "level": "error", "msg": "ThemeChanged handler threw" }
] }
// Response:
{ "success": true, "data": { "accepted": 2 } }
Recognized fields: msg (required; empty or missing drops the event), level (optional, defaults to info; free-form string mapped to server log levels, unknown values log as info), and page (optional; wraps the output as [page] msg). Any other fields on the event object are silently dropped — if you need to capture a stack trace or structured data, concatenate it into msg. Both msg and page are sanitized: CR/LF/NUL stripped (blocks log-line forgery) and length-capped (4096 chars for msg, 256 for page).
Runtime SMTC target management — discover MBXHub endpoints on the network and switch which one the Shell mirrors to Windows Media Transport Controls (taskbar overlay, lock screen, Bluetooth headsets).
Served by the Shell (MBXHub.exe), not the plugin. Listens on smtc.port from mbxhub-shell.json, default 8081 (REST port + 1). The listener emits Access-Control-Allow-Origin: * and handles OPTIONS preflight, so any browser page served by the plugin on 8080 can call this port directly cross-origin.
Current SMTC target (host:port) and connection state. Response shape: { "target": "127.0.0.1:8080", "state": "connected", "controller": { "nodeId": "...", "name": "...", "restPort": 8080 } }.
Switch SMTC target. Body: { "target": "host:port" }. Tears down the existing WS connection, updates Shell config, reconnects to the new endpoint. 422 if the target probe fails.
Cached list of MBXHub endpoints discovered on the network. Each entry: address, name, active (boolean), capabilities (e.g. ["rest-api", "websocket", "player-control", "autoq", "discovery"]). Returns 503 if the SMTC bridge is unavailable.
Clear the cache and re-scan the network via SSDP for MBXHub endpoints. Returns the fresh endpoint list.
v0.5.3.6 — Setlist-aware skip. When the playing file carries a Comment-embedded setlist (the SL-TPS detector returns true on its Comment), /player/next and /player/previous navigate by setlist entry instead of by queue file. next seeks to the next entry's startMs; from the last entry it falls through to Player_PlayNextTrack. previous follows standard transport behaviour: within 2 seconds of an entry's start it seeks to the previous entry's startMs; after 2 seconds it restarts the current entry. From the first entry within the 2 second grace it falls through to Player_PlayPreviousTrack. Response carries scope: "setlist" + entryIndex (1-based) on setlist hits; falls back to the original { result: bool } shape for queue-file skips. Lookup re-parses Comment on each press with a 2 second share-window so consecutive button mashes don't re-parse but a user edit to the Comment is visible within ~2 seconds; track changes invalidate immediately.
PUT /player/volume accepts {"volume":50} (absolute) or {"delta":-5} (relative — resolved server-side against the current volume, clamped 0-100).
/player/update-play-statistics applies a caller-chosen countType (IncreasePlayCount, IncreaseSkipCount, NoChange) to a track (body: { "url", "countType"?, "disableScrobble"? }). Prefer /player/report-play below, which judges played vs skipped server-side. Returns 403 FEATURE_DISABLED when apiDisablePlayCountUpdates is on; gated by player access during party mode; shares the per-IP play-stat rate limit with /player/report-play.
Report a client-side (Listen Here) playback exit. The server applies MusicBee's own play-count thresholds (PlayCountTriggerPercent / PlayCountTriggerSeconds) to decide played vs skipped, then updates play statistics — so browser-streamed plays count exactly like speaker plays. For CUE virtual sub-tracks, send the raw file position plus cueStartMs; the listened window is measured against durationMs (the sub-track duration).
POST /player/report-play
{ "url": "C:\\Music\\track.mp3", "positionMs": 185000, "durationMs": 240000, "cueStartMs": 0 }
// Response:
{ "success": true, "data": { "url": "...", "counted": true, "countType": "IncreasePlayCount",
"playedPercent": 77.1, "playedSeconds": 185, "result": true } }
countType is IncreasePlayCount (past threshold), IncreaseSkipCount (exited early), or NoChange (position 0 — nothing recorded). Optional abandoned: true marks a non-deliberate exit (tab close, output toggle, external track change): past threshold it still counts a play, below it nothing is recorded — never a fabricated skip. Optional cue: true marks a CUE-backed source — required for a sheet's FIRST sub-track, whose cueStartMs is 0. When durationMs is omitted or 0, the server derives it from the CUE sheet for sub-tracks (next track start − this start; the sheet's last track has no derivable end, so only the seconds rule can count it as a play and no skip is ever fabricated) or the library's duration for whole files, so the percent threshold still applies. Counted plays also feed the AutoQ session signal and, when TrueShuffle is enabled, the shuffle played-set and AutoReset completion check — neither feature is required for counting. Duplicate reports from the same client for the same track (keyed url + cueStartMs, so CUE sub-tracks never collide) are absorbed within an adaptive window (deduped: true), and play-statistic updates share a per-IP rate limit (rateLimitPlayStatsPerMinute, 429 when exceeded; loopback exempt). Optional disableScrobble: true suppresses Last.fm scrobbling for this report. 404 if the file is not in the library or apiDisablePlayReporting/apiDisablePlayCountUpdates is enabled.
Returns current track info with full metadata
// Response:
{
"success": true,
"data": {
"playing": true,
"url": "file:///C:/Music/Artist/Album/track.mp3",
"title": "Love Don't Live Here",
"artist": "Breaking Rust",
"album": "Greatest Hits",
"albumArtist": "Breaking Rust",
"year": "2024",
"genre": "Rock",
"trackNo": "3",
"discNo": "1",
"rating": "4.5",
"love": true,
"duration": 234000,
"position": 45000
}
}
Tag fields: TrackTitle, Artist, Album, AlbumArtist, Year, Genre, Rating, RatingLove, Comment, Composer, Conductor, TrackNo, DiscNo, Lyrics, Publisher
Properties: Bitrate, SampleRate, Channels, Duration, Size, DateAdded, DateModified, PlayCount, SkipCount, LastPlayed
Examples:
GET /nowplaying/tag?field=Artist → {"value": "Breaking Rust"}
GET /nowplaying/tag?field=Album → {"value": "Love Don't Live Here"}
GET /nowplaying/tag?field=Genre → {"value": "Rock"}
GET /nowplaying/tag?field=TrackNo → {"value": "3"}
GET /nowplaying/property?type=Bitrate → {"value": "320"}
GET /nowplaying/property?type=Duration → {"value": "234000"} (ms)
GET /nowplaying/property?type=PlayCount → {"value": "42"}
Returns album artwork as binary image or URL
Returns the current track's lyrics. Four response shapes:
{ hasLyrics: false, lyrics: null } — no lyrics and no fallback.{ hasLyrics: true, lyrics, source: "lyrics" } — real MusicBee lyrics, no sibling comment.{ hasLyrics: true, lyrics, source: "lyrics", comment } — real MusicBee lyrics with a sibling Comment tag. Clients can render a Lyrics/Comment toggle.{ hasLyrics: true, lyrics, source: "comment", label } — fallback from the track Comment tag (great for concert setlists, album liner notes). label is the chip text shown above the body in the UI.Both the sibling comment field and the fallback path are configured via the LyricsFallback section (Enabled, MaxDisplayChars, Label) and can be hard-killed via ApiDisableLyricsFallback.
/nowplaying/artist-picture-urls — returns a pictures array with src URLs (serveable via /nowplaying/artist-pictures/{index}) in addition to raw file paths.
/nowplaying/artist-pictures/{index} — serve current artist’s Nth picture as binary image (0-based). Content-Type auto-detected; Cache-Control: max-age=3600. Query: ?localOnly=true (default).
FFT spectrum and waveform data for visualizations
Current stereo peak and RMS levels (0.0–1.0). Returns {peak: [L, R], rms: [L, R]}. Requires MusicBee 3.6+ (API rev 58+).
Returns the now playing list. Supports ?offset=0&limit=50. Each track carries an optional provenance object — who filled it and why (driver: AutoQ | TrueShuffle | Journey | Station | Manual, a short reason, and whenUtc). Omitted for tracks with no stamp (older fills or direct MusicBee queue adds).
// Response:
{
"success": true,
"data": {
"currentIndex": 5,
"total": 150,
"offset": 0,
"limit": 50,
"tracks": [
{
"index": 0,
"url": "file:///C:/Music/track1.mp3",
"title": "First Track",
"artist": "Artist Name",
"album": "Album Name",
"duration": 234000,
"provenance": {
"driver": "AutoQ",
"reason": "mood:Energetic magnet",
"whenUtc": "2026-07-26T12:00:00.0000000Z"
}
},
// ... more tracks (provenance omitted when unknown)
]
}
}
// POST /queue/add - Add tracks to queue (position: "next" or "last")
{"urls": ["file:///C:/Music/song1.mp3", "file:///C:/Music/song2.mp3"], "position": "last"}
// or single track:
{"url": "file:///C:/Music/song.mp3", "position": "next"}
// Response: {"success": true, "data": {"result": true, "added": 2, "position": "last"}}
// POST /queue/playnow - Play track immediately
{"url": "file:///C:/Music/song.mp3"}
// POST /queue/play - Play track at index
{"index": 3}
// POST /queue/move - Move track in queue
{"from": 5, "to": 2}
Query library with ?query=, ?artist=, ?albumArtist=, ?album=, ?genre=, ?people=, ?sort=, ?include=, ?roles=.
v0.5.3.4 — ?people= on album expansion. When ?album=Y&people=X is set the post-filter probes MB's full role-union per track — Artists (144 — Artist + Performer + Guest + Remixer), Composer (43), and Conductor (45) — rather than relying on the ArtistPeople XmlFilter alone. The XmlFilter omits the GuestArtist role bucket, so a track tagged with the queried person as guest (e.g. David Guetta — The Whisperer (feat. Sia)) was previously dropped from expansion even though the album-by-artist count showed it. Whole-library ?people= (no ?album=) still uses the fast ArtistPeople XmlFilter path; pure-guest credits won't surface there but are reachable by drilling into the specific album.
v0.5.3.4 — ?include=<TagName>. Each returned track row carries extraField + extraValue with the per-track value of the requested tag. Accepts any tag from the 48-tag customSorts allowlist: Custom1..Custom16, Virtual1..Virtual25, Year, OriginalYear, SortAlbum, SortAlbumArtist, SortArtist, SortTitle, SortComposer. Unknown values are silently ignored. Used by browse.html to surface the active custom sort's per-track value when expanding an album under a custom-* sort.
v0.5.3.4 — ?roles=true. Adds every per-track role-credit field MB tracks separately: artistsRole (145), performers (146), guests (147), remixers (148), composers (43), conductors (45), artistsUnion (144 — the full role-union). Raw strings preserved (mixed separators across tag implementations). Default off so regular dashboard queries don't pay for the extra Library_GetFileTag reads; only paid for when explicitly requested.
// GET /library/files?artist=Breaking%20Rust&limit=10
{
"success": true,
"data": {
"total": 42,
"offset": 0,
"limit": 10,
"tracks": [
{
"url": "file:///C:/Music/Breaking Rust/track.mp3",
"title": "Love Don't Live Here",
"artist": "Breaking Rust",
"album": "Greatest Hits",
"duration": 234000
},
// ... more tracks
]
}
}
Full-text search: ?q=search+term&sort=alpha (searches title, artist, album, genre; diacritic and punctuation normalized so "cafe" matches "Café" and "acdc" matches "AC/DC"). Two matching modes:
The default mode is controlled by the Library.DisableStrictSearch setting (false by default = strict). Override per-call with ?substring=true|false. The response payload includes a mode field echoing which mode was used.
v0.5.3.0: DSL auto-detection is on by default (library.search.dsl.enabled = true). Qualifier syntax (artist:, album:, genre:, year:, rating:, fmt:, range / boolean / grouping operators) routes through SearchDslParser automatically when detected; plain free-text queries take the strict/substring path as before. Force per-call with ?dsl=true; set library.search.dsl.enabled = false to require the explicit opt-in. Cheat sheet at GET /library/search/syntax.
Unreleased — any-field search. The DSL field registry now covers every MusicBee custom slot: custom1:…custom20: translate to a Field="CustomN" condition (e.g. custom7:live; Custom17–20 arrived with MusicBee 3.5). Combine with existing qualifiers as usual (albumartist:"various" custom7:live). Mood-position filtering uses the first-class arousal:/valence: range qualifiers (0–1, e.g. arousal:>0.7) — see the qualifier table above.
v0.5.3.0 backstop: queries shorter than search.live.minQueryLength (default 2) return 400 QUERY_TOO_SHORT with body { "error": "Query must be at least N characters (got M)." }. Single-char walks visit every track on a 200k library (~4 s) and stall every other MB-API consumer for the duration of the cursor lock; the backstop protects fleet-wide responsiveness from runaway non-conforming clients (curl, scripts). Set search.live.minQueryLength = 0 to disable.
v0.5.3.0. Returns the DSL grammar as JSON (qualifiers, operators, worked examples). Authoritative source — the reference below is rendered from the same data shape. Used by Cmd+K's cheatsheet and any other client surfacing DSL syntax to users.
Response shape: { version, qualifiers: [{ name, type, allowsRange, allowsOperators, filterable, mapping, enumValues }], operators: [{ symbol, description }], examples: [{ query, description }] }. filterable (bool) marks the qualifiers the AutoQ candidate filter can evaluate — a filter-scoped client derives its allow-list from this flag rather than hardcoding one.
The DSL layers on top of free-text search: every plain word still matches normally, qualifiers narrow the result, operators compose. Auto-detected on /library/search and /search when library.search.dsl.enabled = true (default); force per-call with ?dsl=true.
| Qualifier | Type | Range / Ops | Maps to | Notes |
|---|---|---|---|---|
artist: | string | — | ArtistPeople | Includes featured / album artists. |
album: | string | — | Album | |
albumartist: | string | — | AlbumArtist | Album-level artist (compilation-aware). |
genre: | string | — | Genre | Multi-value genre tags split client-side. |
year: | int | range + ops | Year | e.g. year:1985, year:1985..1990, year:>=2000. |
decade: | enum | — | derived from Year | Values: 60s, 70s, 80s, 90s, 00s, 10s, 20s. |
rating: | int | range + ops | Rating (0–5) | e.g. rating:>=4, rating:3..5. |
loved: | bool | — | Loved tag | loved:true / loved:false. |
bpm: | int | range + ops | Tempo (Truedat / Essentia) | Requires fingerprint or mood-cache data. |
mood: | string | — | AutoQ mood channel | Channel name (e.g. mood:chill). Post-filter — runs after candidate selection. |
vibe: | float | range + ops | AutoQ vibe score | 0–1. e.g. vibe:>=0.7. |
source: | enum | — | MB Source Type | Values: library, inbox, audiobooks, videos, podcasts. Default from library.search.dsl.defaultSource. |
playlist: | string | — | playlist filter | Post-process — intersects with named playlist membership. |
added: | date | range + ops | DateAdded | Accepts absolute dates (2025-01-01) and relative (now-7d); user aliases via library.search.dsl.dateAliases. |
played: | date | range + ops | DateLastPlayed | Same date format as added:. |
playcount: | int | range + ops | PlayCount | e.g. playcount:>10, playcount:0 (never played). |
duration: | int (seconds) | range + ops | Duration | e.g. duration:>3600 (over an hour). |
path: | string | — | FilePath substring | Case-insensitive substring match on the full path. |
type: | string | — | file extension | Post-process — exact, case-insensitive match on the file extension (e.g. type:flac, type:mp3). Dot optional. Combine formats with OR. |
key: | string | — | Camelot mix key | Post-process — exact, case-insensitive match on the track’s Camelot code from the mood cache (e.g. key:9A, key:11B). Scanned tracks only — no key data never matches. Combine wheel-compatible codes with OR: key:9A OR key:9B OR key:8A OR key:10A. |
lyric: | string | — | lyrics body | Post-process — fetches lyrics per candidate. Expensive; gated on library.search.dsl.allowLyricSearch = true (default off). |
| Symbol | Meaning | Where it applies |
|---|---|---|
> | greater than | Qualifiers with allowsOperators. |
>= | greater than or equal | Same. |
< | less than | Same. |
<= | less than or equal | Same. |
.. | inclusive range | Qualifiers with allowsRange. low..high — both ends inclusive. |
- (prefix) | exclude | Negates a free-text term or qualifier (e.g. -genre:metal, -live). |
OR / or | boolean OR | Between sibling expressions. Default between terms is AND. |
( ... ) | grouping | Forces precedence inside a larger expression. |
| Query | Result |
|---|---|
artist:radiohead | Tracks by Radiohead. |
year:1985..1990 | Tracks released between 1985 and 1990 inclusive. |
year:1965 rating:>4 | 1965 tracks rated above 4 — combines two range/operator qualifiers (implicit AND). |
mood:chill rating:>=4 | Chill-mood tracks rated 4 or higher — mood is a post-filter applied after the rating cut. |
rock -genre:metal | Rock tracks excluding the metal genre — free-text plus exclusion. |
(rock or metal) -live | Rock or metal, but not live recordings — boolean OR plus exclusion. |
source:audiobooks duration:>3600 | Audiobooks longer than one hour — source scope plus duration operator. |
played:<now-30d playcount:>5 | Favorites you haven't played in the last 30 days — relative date plus playcount. |
decade:80s -genre:disco | 80s, no disco — decade enum plus genre exclusion. |
OR (or lowercase or) for disjunction; parentheses for grouping.radiohead year:>=2000 filters Radiohead tracks from 2000 onward.mood, playlist, lyric) run after the candidate set is fetched, so they're capped by library.search.dsl.maxPostFilterCandidates (default 5000).422 INVALID_DSL with a parse position and Levenshtein-based “did you mean” suggestions.2025-01-01), relative (now-7d, now-30d), or named alias from library.search.dsl.dateAliases (defaults: lastweek, lastmonth, thisyear).v0.5.3.0 federated search. Runs typed buckets (tracks / albums / artists / playlists / saved) in parallel and returns a unified response with cursor pagination on the tracks bucket, facet counts, and a top-hit. Backs the Cmd+K palette and (v0.5.3.0+) the dashboard search bar, player.html, browse.html, and explore.html.
DSL routing (v0.5.3.0 post-launch): /search auto-detects DSL queries the same way /library/search does — qualifier colons (year:, rating:, etc.), .. ranges, >/< comparisons, OR , leading - exclusion. When detected and Search.Dsl.Enabled=true (default), the query routes through the DSL pipeline and the response carries mode: "dsl". Per-call ?dsl=true still works as an explicit opt-in. Plain free-text queries continue to use strict/substring mode as before. Tracks bucket is sorted by match-strength score (title > album > artist, with prefix-match bonus) so the most-relevant result appears first regardless of mode.
Params: ?q=&buckets=tracks,albums,artists,playlists,saved&limit=N&cursor=opaque. Cursors are HMAC-stamped + TTL-checked; stale → 410, malformed → 400, query-mismatch → 400. Same QUERY_TOO_SHORT backstop as /library/search. v0.5.3.0: per-bucket ?limit= hard cap raised from 200 to 500 (browse.html aggregates client-side over the tracks bucket and needs the headroom).
v0.5.3.0. Live engine + index state snapshot. Currently reports engine: "mb" (FTS5 substrate dropped during the post-decouple harvest; seam preserved for future engines).
Persist a query under a name and re-run on a schedule. Backed by mbxhub-search.json next to mbxhub.json. Disabled by default; enable via library.searchDsl.savedSearch.enabled. Disabled endpoints return 404 NOT_FOUND. Background SavedSearchScheduler ticks each saved search's interval, evaluates, diffs against last-match URLs, and broadcasts the SearchMatched WebSocket event when matches change.
Validation: name required (1–80 chars, unique per host case-insensitive → 409 on duplicate); query parsed via the DSL (422 INVALID_DSL on parse error with parse position).
Mutation routes (POST / PUT / DELETE / run) return 403 FORBIDDEN when ApiReadOnlyMode is set — kiosk deployments can’t have saved searches mutated by remote clients.
Server-side recent-search log shared by Cmd+K and any other search bar. Falls through to client localStorage if the endpoint is missing or returns 404.
GET ?limit=N — newest-first. limit defaults 20, caps at 200. POST body {query, source} — empty body clears (returns {cleared:true}). DELETE — wipe all entries.
POST and DELETE return 403 FORBIDDEN when ApiReadOnlyMode is set.
/library/albums/detailed returns albums with firstTrackUrl, year, dateAdded, and virtualKind ("cue" | "setlist" | null) for artwork lookups and sorting. virtualKind identifies CUE-split or setlist virtual tracklists; drives the CUE / SET LIST badge in browse and on the dashboard now-playing card. Eliminates per-album /library/files?limit=1 round-trips. Params: ?offset=&limit=
/library/album-artists returns distinct album artists. Params: ?offset=&limit=
/library/albums/unheard returns albums where all tracks have playCount=0. /library/albums/with-pdf returns albums containing PDF booklets. /library/albums/with-video returns albums whose folder contains a video file (derived from the Video library, Source Type 64, matched by folder). All support ?offset=&limit=
/library/inbox, /library/audiobooks, and /library/videos query MusicBee library categories (Source Types 4, 32, 64). Browse page shows these tabs only when non-empty (progressive reveal).
Evaluate a MusicBee expression: ?expression=<Artist> - <Title>&fileUrl=C:\Music\track.mp3. If fileUrl is omitted, evaluates against the currently playing track. Returns {expression, fileUrl, result}. Supports MusicBee template syntax (<Artist>, $If(), virtual tags).
MusicBee’s built-in placeholder image for tracks with no artwork. Returns binary image data with appropriate content type. Cache-Control: 24h.
Albums for an artist with year, track count, firstTrackUrl for artwork, and virtualKind ("cue" | "setlist" | null). Params: ?albumArtist= or ?artist= (one required), ?sort=alpha|year|year-asc. ?artist= queries ArtistPeople (broader match), ?albumArtist= queries AlbumArtist (exact album credit).
// GET /library/albums/by-artist?albumArtist=Pink%20Floyd&sort=year
{
"success": true,
"data": {
"albumArtist": "Pink Floyd",
"total": 3,
"sort": "year",
"albums": [
{
"name": "The Dark Side of the Moon",
"year": "1973",
"count": 10,
"firstTrackUrl": "file:///C:/Music/Artist/Album/track.mp3"
}
]
}
}
Note: URL-encode the file path in the URL (e.g., /library/file/file%3A%2F%2F%2FC%3A%2FMusic%2Ftrack.mp3)
// GET /library/file/{url} - Extended track metadata (includes playCount, lastPlayed, etc.)
{
"success": true,
"data": {
"url": "file:///C:/Music/Artist/Album/track.mp3",
"title": "Love Don't Live Here",
"artist": "Breaking Rust",
"album": "Greatest Hits",
"duration": 234000,
"albumArtist": "Breaking Rust",
"genre": "Rock",
"year": "2024",
"trackNo": "3",
"discNo": "1",
"rating": "4.5",
"composer": "J. Smith",
"bitrate": "320",
"format": "MPEG Audio",
"sampleRate": "44100",
"playCount": 42,
"dateAdded": "2024-01-15",
"lastPlayed": "2024-01-20"
}
}
// PUT /library/file/{url} - Update metadata (fields: title, artist, album, albumArtist, genre, year, trackNo, discNo, composer, comment, rating)
{"rating": "5", "comment": "Great track!"}
// Response: {"success": true, "data": {"result": true, "updated": ["rating", "comment"]}}
// POST /library/artwork/batch - Batch artwork fetch (max 50 URLs per request)
// Request: {"urls": ["D:\\Music\\track1.mp3", "D:\\Music\\track2.flac"]}
// Response: {"success": true, "data": {"D:\\Music\\track1.mp3": "data:image/jpeg;base64,/9j/...", "D:\\Music\\track2.flac": null}}
// POST /library/commit - Commit pending tag changes to file
// Use after batching Library_SetFileTag RPC calls
// Request: {"file": "D:\\Music\\track.mp3"}
// Response: {"success": true, "data": {"result": true}}
/artwork-count returns 0 if the file has no embedded artwork, 1 otherwise. Internally probes up to 20 embedded-artwork locations (MusicBee returns the same cover at multiple locations — EmbedInFile, LinkToSource, FolderThumb —) and picks the largest byte-size as the canonical image; subsequent /artwork?index=0 requests serve that best variant. Cached per fileUrl. /pdf serves the PDF booklet from the track's album folder. /has-pdf checks existence without downloading.
v0.5.3.5. When a track's Comment tag contains a time-coded set list (≥3 monotonically increasing entries — common on bootlegs / live broadcasts / podcasts), these endpoints surface the parsed entries and let one click write a real CUE sidecar. Parsing follows a formal grammar (SL-TPS v1.0).
Accepted line formats. Each candidate line is tested independently (multiline-anchored regex, ^…$), with an optional leading bullet (- / • / *) and an optional decorative index prefix (digits, optional . or ), optional - / en-dash / em-dash). After those, the line must match one of two core shapes:
[HH:]MM:SS [optional separator] TITLETITLE [optional separator] [HH:]MM:SSThe optional separator between time and title accepts -, –, —, or : followed by whitespace (the colon variant is common on YouTube-comment paste-throughs, e.g. 02:46 : Last Train). The timestamp itself may also be wrapped in parens, e.g. (0:11) Live In The Moment — common on YouTube / Reddit comment setlists; title-internal parens (e.g. Don't Look Back In Anger (Oasis cover)) are preserved verbatim. Hours are optional; minutes 1-2 digits; seconds exactly 2 digits. Worked examples that all match: 0:40 - Catch These Fists, 0:00:00 Sweet Lies, 01 - The SoundMaker - 00:00, 1. Amyl And The Sniffers - Foo 00:00, Man Made Of Meat 01:09, 02:46 : Last Train, (0:11) Live In The Moment. Index prefixes in the text are decorative; the parser assigns sequential 1, 2, 3, … from match order (intrinsic ordering per spec §5). Lines that don't match (URLs, freeform notes, blank lines) are silently skipped — they don't kill detection, they just don't contribute to the count.
GET /library/file/{urlHash}/setlist returns { fileUrl, fileDurationMs, entries:[{index, startMs, title, rawTime, cueTime}], trailingNotes, parsedAt, extractedCueExists, extractedCuePath }. Each entry carries the raw matched timecode (rawTime, e.g. "0:40") and the CUE MM:SS:FF form (cueTime, e.g. "00:40:00", 75 fps) so consumers don't re-derive them. fileDurationMs is the parent file's length so the Tracklist UI can compute per-entry durations including the trailing entry — 0 when a paired CUE exists (signals: trailing-duration math unreliable). 404 NO_SETLIST if the Comment doesn't match the heuristic.
Computing {urlHash}. The input is the track's url — the url field returned by /library/files or /nowplaying. The hash is: SHA-1 of the UTF-8 bytes of that exact url string → keep the first 16 bytes → base64-encode → make URL-safe (+→-, /→_, strip trailing =). Result is a 22-char string. Example pseudocode: base64url(sha1(utf8(url))[0:16]). Browser clients can call MBXShared.urlHashOf(url) (shared.js), which returns the identical value on both secure and non-secure (LAN HTTP) contexts.
POST /library/file/{urlHash}/extract-setlist-cue, body { overwrite: bool } (default false): parses the Comment, builds a real CUE sidecar at <audio-basename>.cue (UTF-8 with BOM), written atomically (temp → replace → .bak). On success the album index is invalidated so the next browse / search / now-playing read returns the virtual tracks. Errors: 409 CUE_EXISTS when a sidecar already exists and overwrite=false (client switches CTA to “Replace existing CUE”), 400 NO_SETLIST, 500 WRITE_FAILED.
Consumers: ?setlist=hint on /library/files emits hasSetlist:bool per row (browse drilldowns surface the SET LIST badge). /nowplaying emits hasSetlist for the current track (drives the dashboard SET LIST chip, the Tracklist tab on nowplaying.html, and the wavescrubber chapter ticks on play.html — the latter pulls entries from /library/file/{urlHash}/setlist when MB is still playing the parent file as a single item, before CUE extraction virtualises it). After extraction the album's virtual tracks expose cueStartMs per row.
Duration sanity: a setlist's last timestamp must fall before the file's duration (catches Comments pasted from a different recording); otherwise detection returns false. Concurrent extracts can't race (per-call temp file) and failed writes don't leak it. Requires library-tag write access, so PartyMode guests cannot write.
Deep-link query params (no new endpoints): /pages/nowplaying.html?tab=tracklist opens the standalone page with the segmented control switched to Tracklist on first successful setlist load; used by the dashboard SET LIST badge. /pages/play.html?np-tab=tracklist opens the full-player chrome and propagates &tab=tracklist to the middle-pane NP iframe — used when DashboardLayout.liveSetBadgeTarget is set to "play" (default "nowplaying"). /pages/browse.html?album=&artist=&trackUrl=<hash> scrolls the matching row into view with a brief accent pulse after the drilldown renders; generated by the Now Playing right-click “Copy deep link”.
List all images in the track's album folder and one level of subfolders. Returns folder path, image paths, and count. Excludes: canonical primary-cover filenames (folder.jpg, cover.jpg, front.jpg, album.jpg + .jpeg/.png siblings — these are duplicates of the primary artwork served by /artwork); all Windows Media Player cache files (AlbumArtSmall*, AlbumArt_*); thumbnail artifacts (<5KB); Thumbs.db; desktop.ini.
Security: track must be in the MusicBee library.
Serve a single fan art image by absolute path (binary). Returns image with appropriate content-type. Cache-Control: max-age=3600.
Security: image must reside in a directory that contains at least one MusicBee library file.
v0.5.3.5. List video files in the track's album folder (+ one subdir level). Filters by extension: .mp4, .mkv, .webm, .mov, .m4v, .avi. Returns { folder, videos: [{ path, name, sizeBytes }], count }. Drives the Video tab inside the Extras panel on nowplaying.html and the Videos section on explore.html's expanded album view.
Security: track must be in the MusicBee library.
v0.5.3.5. Serve a single video file by absolute path (binary). Content-Type by extension: video/mp4, video/x-matroska, video/webm, video/quicktime, video/x-msvideo. Cache-Control: max-age=3600.
Security: same as /library/fan-art/{path} — the path's directory (or its parent) must contain at least one MusicBee library file.
/library/artist/{name}/pictures — returns a pictures array with src URLs (serveable via /library/artist/{name}/pictures/{index}) in addition to raw file paths. Query: ?localOnly=false.
/library/artist/{name}/pictures/{index} — serve artist picture by 0-based index as binary image. Content-Type auto-detected (falls back to image/jpeg for MusicBee cache files); Cache-Control: max-age=3600. Query: ?localOnly=true (default).
Recently played tracks sorted by last played descending. Params: limit (1–200, default 50), offset (default 0), days (1–365, default 30). Returns tracks with lastPlayed, playCount, skipCount fields.
Video files from MusicBee’s Video library node (Source Type 64). Returns {total, videos: [{url, title, artist, album, kind, duration}]}. Title falls back to filename when tag is empty. Stream video files via /stream/{url}.
/library/files/raw — raw MusicBee file data without CUE processing. Optional ?album= filter. Max 50 results.
/library/cuetest — test CUE track resolution for a query. Param: ?query=
List radio stations from MusicBee’s Radio node.
// Response:
{
"success": true,
"data": {
"total": 5,
"stations": [
{"url": "http://stream.example.com/radio", "name": "Jazz FM"},
// ...
]
}
}
// GET /playlists - List all playlists
{
"success": true,
"data": {
"total": 5,
"playlists": [
{"url": "playlist://Favorites", "name": "Favorites", "trackCount": 120},
{"url": "playlist://Workout", "name": "Workout", "trackCount": 45},
// ... more playlists
]
}
}
// POST /playlists - Create new playlist
{"name": "New Playlist", "folder": "", "files": ["file:///C:/Music/song1.mp3"]}
// Response: {"success": true, "data": {"url": "playlist://New Playlist", "name": "New Playlist", "trackCount": 1}}
// PUT /playlists/{url} - Replace playlist contents
{"files": ["file:///C:/Music/song1.mp3", "file:///C:/Music/song2.mp3"]}
// GET /playlists/{url}/files - Get playlist tracks (supports pagination: ?offset=0&limit=50)
{
"success": true,
"data": {
"total": 120,
"offset": 0,
"limit": 50,
"tracks": [
{"index": 0, "url": "file:///...", "title": "Song", "artist": "Artist", "album": "Album", "duration": 234000},
// ... more tracks
]
}
}
// POST /playlists/{url}/files - Add tracks to playlist
{"urls": ["C:\\Music\\song.mp3"]}
List podcast subscriptions. Query: ?query=
/podcasts/{id}/episodes/{index} — get a specific episode by numeric index.
Flat episode listing. Query: ?id= (feed URL). Use this when the subscription ID is a URL rather than a simple ID.
Read-only passthroughs to MusicBee’s own settings API. For MBXHub’s configurable hub settings, see /system/settings above.
Overview bundle of the most common settings in one response.
// Response:
{
"success": true,
"data": {
"storagePath": "C:\\Users\\...\\MusicBee",
"skin": "Default",
"windowBordersSkinned": false,
"lastFmUserId": "scott365",
"webProxy": null
}
}
GET /settings/storage-path — Persistent storage pathGET /settings/skin — Current skin nameGET /settings/skin-element-color — Skin color. Query: ?element=SkinSubPanel&state=ElementStateDefault&component=ComponentBackgroundGET /settings/window-borders-skinned — Whether window borders are skinnedGET /settings/lastfm-user — Last.fm user IDGET /settings/web-proxy — Web proxy configurationGET /settings/field-name?field={MetaDataType} — Display name for a metadata field (e.g. TrackTitle, Custom1)GET /settings/data-type?field={MetaDataType} — Data type for a metadata fieldGET /settings/value?id={SettingId} — Raw MusicBee setting by ID (e.g. CompactPlayerFlickrEnabled). 400 with the full valid-ID list if unknown.GET /settings/convert-command?codec=Mp3&quality=HighQuality — File-conversion command line for a codec/quality pairGracefully close MusicBee. Requires allowRemoteExit=true in settings.
Optional body to schedule a restart via Windows Task Scheduler before closing:
// Request (optional):
{
"restart": true, // schedule restart before closing (default: false)
"delay": 22 // seconds to wait before restarting (default: 22, range: 1-300)
}
// Response:
{
"success": true,
"data": {
"message": "MusicBee restarting in 10s",
"restart": true,
"delay": 22
}
}
Restart MusicBee. Convenience alias for /app/exit with restart=true. Requires allowRemoteExit=true.
// Request (optional):
{
"delay": 22 // seconds to wait before restarting (default: 22, range: 1-300)
}
Turn MusicBee into a social jukebox. Guests scan a QR code, enter a PIN, and request songs from their phone. The DJ controls playback while a TV display shows artwork, lyrics, and a live feed of requests and joins.
Roles: Guest (browse/request), DJ (full control), Display (TV mode)
Kill switch: set ApiDisablePartyMode = true in mbxhub.json
(Features section) to disable party mode entirely. All /partymode/* routes return 404 NOT_FOUND
and /pages/partymode/* static pages return 404. Live state is exposed at
/system/features as partymode: false.
Streaming during a party: the raw-byte serving endpoints — GET /stream/*
(audio), GET /library/video/*, and GET /library/file/{url}/pdf (booklets) —
return 404 while a party is active unless partyAllowStreaming is enabled (default false;
PartyMode dialog or web settings). /system/features reports the effective audio state as
streaming.
Get current party state (active, request count).
Start a party session. Host-only: caller must be on the host machine
(loopback / request.IsLocal). Tablets, phones, and other LAN devices become DJ via
/partymode/verify-dj after the host has started the party.
// Request body:
{"guestPin": "1234", "djPin": "5678"}
// djPin is optional - defaults to guestPin
// Errors:
// 403 PARTY_START_FORBIDDEN — caller is not on the host machine
// 409 PARTY_ALREADY_ACTIVE — a party is already running; stop it first
End the current party session.
Validate PIN and get role. If nickname provided, announces join in feed.
// Response:
{"success": true, "data": {"valid": true, "role": "guest"}}
// role: "guest" or "dj"
Verify if a PIN grants DJ access. Used by DJ page login.
// Request body:
{"pin": "5678"}
// Response:
{"success": true, "data": {"valid": true}}
// valid is true only if PIN matches DJ PIN
Submit a vote (thumbs up/down) with guest attribution. Records in feed and forwards to influences if AutoQ available.
// Request body:
{"type": "++", "target": "Artist", "value": "Daft Punk", "nickname": "Haro"}
// type: "++"=more, "--"=less; target: "Artist" or "Genre"
Submit a song request (adds to queue).
// Request body:
{"url": "C:\\Music\\song.mp3", "nickname": "Haro"}
// Response includes requestId, title, artist
Get party feed (joins, requests, votes, reactions - newest first).
// Response:
{"success": true, "data": {
"items": [
{"type": "join", "nickname": "Haro", "timestamp": "..."},
{"type": "request", "nickname": "Haro", "title": "Song", "artist": "Artist", "timestamp": "..."},
{"type": "voteup", "nickname": "Haro", "artist": "Daft Punk", "timestamp": "..."},
{"type": "votedown", "nickname": "Haro", "artist": "Nickelback", "timestamp": "..."},
{"type": "reaction", "nickname": "Haro", "emoji": "🔥", "title": "Song", "artist": "Artist", "timestamp": "..."}
]
}}
Get recent song requests only (for DJ page).
Generate QR code PNG image. Auto-includes PIN if party is active.
Returns the caller's role based on IP: host (loopback), dj (verified DJ PIN), or guest.
Party Mode settings are configured in MusicBee via Settings → Network → Party Mode...
| Setting | Default | Description |
|---|---|---|
protectMetadata | true | Guards the RPC ActionCategory.Metadata path (the Pending_* tag-editor methods) and the Auto-Heart write. Does NOT gate the manual love / rate / tag-edit endpoints — those are ActionCategory.LibraryTags, governed by the read-only flags (apiReadOnlyLibraryTags et al.). |
rateLimitEnabled | true | Master switch for per-IP rate limiting — party endpoints plus the reaction and client-log buckets. The play-statistic bucket is deliberately independent (disable via rateLimitPlayStatsPerMinute: 0) |
rateLimitRequestsPerMinute | 5 | Max song requests per minute per IP (when rate limiting enabled) |
rateLimitVotesPerMinute | 5 | Max votes per minute per IP (when rate limiting enabled) |
rateLimitPinAttemptsPerMinute | 5 | Max PIN validation attempts per minute per IP |
trustForwardedFor | false | Trust X-Forwarded-For header for client IP. Enable only behind a reverse proxy. |
Protect Metadata: When enabled, blocks POST /dashboard/love, POST /dashboard/rate/{N},
and PUT /library/file/* tag edits for everyone during the party. Use this to prevent guests from permanently modifying
your library ratings. The setting also blocks the DJ—toggle it off temporarily if you need to make edits.
Built-in pages at /pages/partymode/:
index.html - PIN + nickname entry (guest join page)guest.html - Browse 7 tabs (Albums, Artists, Genres, Playlists, Podcasts, Radio, Moods), fuzzy search, request songs, vote on vibesdj.html - Start/stop party, set PINs, manage queue, see requests & vibesdisplay.html - TV mode: artwork, lyrics, request feed, QR code, floating reactionsleaderboard.html - Party stats: guest activity, top tracks, reaction countsIntelligent queue system. AutoQ combines TrueShuffle rules, mood analysis, reactions, and influences to automatically queue tracks that match the room's energy. The native control surface is the AutoQ Workbench — MusicBee's AutoQ tab (navigator entry under Services; also floating via Tools → MBXHub): list builder, mood tools, programs, and behavior tuning in one place. The Queue tab's setup rows (station, flow, filter) collapse behind a chevron — remembered across restarts — and the layout tracks the tab's width, so it works docked narrow or wide.
TrueShuffle manages the shuffle cycle — play rules, cycle tracking, and queue constraints.
Returns 503 SERVICE_UNAVAILABLE if TrueShuffle/AutoQ not enabled.
Get shuffle cycle status
// Response:
{
"success": true,
"data": {
"enabled": true,
"totalTracks": 1000,
"playedCount": 250,
"remainingCount": 750,
"percentComplete": 25.0,
"cycleStarted": "2024-01-01T00:00:00Z"
}
}
Reset the shuffle cycle. All tracks become unplayed.
Tracks played/remaining in shuffle cycle. Query: ?offset=&limit=
Permanently excluded tracks. Banned tracks are never queued by AutoQ.
Returns 503 SERVICE_UNAVAILABLE if TrueShuffle/AutoQ not enabled.
Get list of banned tracks. Query: ?offset=&limit=
// Response:
{
"success": true,
"data": {
"total": 5,
"offset": 0,
"limit": 50,
"tracks": [
{
"url": "file:///C:/Music/corrupted.mp3",
"reason": "Audio corruption detected at 2:30",
"bannedAt": "2024-01-01T12:00:00Z"
}
]
}
}
// POST /banlist - Ban a track
{"url": "file:///C:/Music/track.mp3", "reason": "Corrupted audio at 2:30"}
// DELETE /banlist/{url} - Unban a track (URL-encode the file path)
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.
Negative (--): Hard exclude matching tracks. Positive (++): Preference boost (future).
Get list of all influences. Query: ?offset=&limit=
// Response:
{
"success": true,
"data": {
"total": 2,
"offset": 0,
"limit": 50,
"influences": [
{
"type": "--",
"target": "Genre",
"value": "Audiobook",
"timestamp": "2024-01-15T10:30:00Z"
},
{
"type": "++",
"target": "Artist",
"value": "The Beatles",
"timestamp": "2024-01-15T10:35:00Z"
}
]
}
}
Get current track's genre/artist and any matching influences (for UI state).
// Response:
{
"success": true,
"data": {
"genre": "Rock",
"artist": "Pink Floyd",
"genreInfluence": null,
"artistInfluence": "++"
}
}
// POST /influences - Add an influence
{"type": "--", "target": "Genre", "value": "Audiobook"}
// type: "++" (more) or "--" (less/exclude)
// target: "Genre" or "Artist"
// DELETE /influences/Genre/Audiobook - Remove specific influence
// POST /influences/clear - Clear all influences (reset preferences)
Get AutoQ status and configuration.
// Response:
{
"success": true,
"data": {
"enabled": true,
"mode": "autopilot",
"soloMode": false,
"vibeListCount": 10,
"vibeListPreview": [
{ "title": "Uptown Funk", "artist": "Mark Ronson", "score": 12.5 }
]
}
}
Start AutoQ. Begins monitoring queue and adding tracks when needed.
// Optional request body:
{ "mode": "autopilot" }
// Modes: "autopilot" (default), "djassist" (aliases: "dj", "assist", "auto")
Stop AutoQ. Queue continues playing but no new tracks are added automatically.
Reset AutoQ session state (clears reactions, taste vector, ban list). DJ-only in party mode.
Force refresh vibe list. Returns updated track count.
// Response:
{
"success": true,
"data": { "message": "Vibe list refreshed", "count": 100 }
}
Refresh the vibe list and immediately enqueue picked tracks in a single call (vibe-list/refresh + queue action combined). Used by the dashboard’s refresh button.
Pick the next track from the vibe list without queueing it.
// Response:
{
"success": true,
"data": { "url": "C:\\Music\\track.mp3", "title": "Uptown Funk", "artist": "Mark Ronson" }
}
Unban a track, allowing it back into the vibe list.
// Request:
{ "url": "C:\\Music\\track.mp3" }
// Response:
{
"success": true,
"data": { "result": true, "url": "C:\\Music\\track.mp3" }
}
Check if the currently playing track is banned.
// Response:
{
"success": true,
"data": { "url": "C:\\Music\\track.mp3", "isBanned": false }
}
AutoQ-Radio run-state: running, pickMode, source (mood/seed), target mood, seed count, flow, tightness. genreQuotaExempt is the run's resolved flag (spec 2026-07-09-genre-quota-per-stream.md) — a saved station's own flag on a station replay, true by default on a plain seeded start, the target mood channel's own flag on a mood run. tightness is the run's live Tight↔Loose dial (see below).
// Response:
{
"success": true,
"data": {
"running": true,
"pickMode": "weighted",
"source": "mood",
"mood": "Energetic",
"seedCount": 0,
"flow": "smooth",
"tightness": 0.38,
"startedUtc": "2026-07-07T12:00:00Z",
"genreQuotaExempt": false
}
}
Start continuous queue top-up. Body {"mode": "fresh"|"continue", "source": "mood"|"seed", "flow": "smooth"|"wave"|"build"|"winddown"} — fresh clears the queue first; source and flow are optional. 409 AUTOQ_DISABLED when AutoQ is disabled.
Stop the radio. The queue is left untouched.
Set the run’s flow live. Body {"flow": "smooth"|"wave"|"build"|"winddown"|<drop-in name>} — unknown names degrade to smooth. Applies to the next fill’s ordering (an already-spliced journey arc is static); persists with the run detail while on-air. GET /autoq/radio returns flows (built-ins + drop-ins) for pickers; the MusicBee panel and the web strip both carry the picker (chrome-sets-flow ruling).
Set the run’s Tightness live. Body {"tightness": 0.0–1.0} — 0 = Tight, 1 = Loose. 400 INVALID_REQUEST when the value is missing, non-numeric, NaN, or outside [0,1]. Applies to the next fill and persists with the run detail while on-air; GET /autoq/radio echoes the current tightness. Tightness is one dial that controls how adventurous a run is: it widens or narrows the candidate funnel reach and weights a continuity term that flows each pick from the previous track across acoustic timbre, harmonic key (Camelot) and half/double-aware tempo. Tight yields coherent, DJ-style harmonically-mixed flow; Loose roams wide for variety. Computed on the library’s own analysis — no tagging required. Surfaced as a Tight↔Loose slider on the radio strip (play / dashboard) and as granular Reach + Continuity sliders in the Workbench Mood tab; persists into a saved Station.
Mode-C reviewable list: body {"seedUrls": [...], "flow": "smooth"|"wave"|"build"|"winddown", "count": 25} → ordered tracks like the seeds; never queues. 400 INVALID_REQUEST without seedUrls; 409 AUTOQ_DISABLED when AutoQ is off.
Journey generate: a finite arc from a start track to an end track (optional midpoint) through mood space, following the selected flow. Body {"waypoints": ["start", ("via",) "end"], "flow": "smooth"|"wave"|"build"|"winddown", "count": N}. Waypoints must have analyzed mood data (400 WAYPOINT_NOT_SCANNED otherwise). Count omitted = distance-derived default (15–30); explicit count clamped 5–50. Or size by time instead: "targetMinutes": M (5–600) fills until that much music is accumulated, overshooting by at most one track — mutually exclusive with count (400 INVALID_REQUEST when both are sent). Response matches /autoq/radio/generate. Never queues; never touches the radio run-state.
Connect-from-queue: the queue-ahead window (exactly 2–3 tracks) is the waypoint set — the generated arc replaces that window, so the user's own tracks still open and close the journey. Body optional: {"flow": "smooth"|"wave"|"build"|"winddown", "count": N, "targetMinutes": M}. Count omitted = distance-derived default (15–30); explicit count clamped 5–50; targetMinutes (5–600) sizes by time (overshoot ≤ 1 track), mutually exclusive with count. Any other queue shape (0, 1, or 4+ tracks ahead) returns 400 QUEUE_SHAPE carrying a teach message clients show verbatim (“Queue 2-3 tracks next, then Connect fills the journey between them.”). Also 400 QUEUE_READ_FAILED / WAYPOINT_NOT_SCANNED / GENERATE_EMPTY / QUEUE_WRITE_FAILED; 409 AUTOQ_DISABLED; party mode gates it like /autoq/radio/start. Never arms the radio; never starts playback. Response: {message, count, flow, replaced, tracks: [{url, title, artist, genre}]} — no score field.
Send to AutoQ: one verb, meaning derived from run state. Body {"files": ["url", ...], "as": "auto"|"start"|"seed"|"waypoint"|"destination"} — as optional, default auto. Nothing on air → starts a station seeded from the sent tracks (sendIdleStart=tray answers collect instead and leaves the tray to the surface). Radio on air → sent tracks queue next and become the run's new seed generation per the Default Send to Q Behavior mode (sendMode). A journey in flight (spliced via /autoq/radio/connect/queue) → sent tracks become waypoints on a re-routed remaining arc toward the same destination (cap sendWaypointCap); the destination itself never moves.
Where the verb lives: MusicBee’s own right-click menu (Send to AutoQ on any selection — outcome shown on MusicBee’s status bar), browse track rows (📨) and the multi-select batch bar, the Cmd+K palette (Alt+Enter on a track row), and this endpoint for scripts. All surfaces route through the same dispatch, so the behavior above is identical everywhere; each send announces what it did (started / steered / waypoint).
Journey-detection scope: only arcs queued via POST /autoq/radio/connect/queue stamp a tracked destination — arcs generated via /autoq/radio/connect and queued by generic queue verbs are not tracked, so a send after one dispatches as steer/start instead of waypoint. A stamped destination that is no longer ahead in play order (the journey finished, or the queue moved past it) answers 409 NO_JOURNEY for both auto and explicit as=waypoint — the stamp is never lazily cleared on a miss, only the run lifecycle clears it. as=destination starts a fresh tracked journey from any state, including from a single sent track (the arc is prepended from now-playing or the nearest scanned queue-ahead track).
Response: {action: "started"|"steered"|"waypoint"|"collect", mode, queued, seeds, waypoints}. Errors: 400 INVALID_REQUEST / WAYPOINT_NOT_SCANNED / JOURNEY_EMPTY; 409 NO_JOURNEY / AUTOQ_DISABLED; 403 in read-only mode, or when a party guest (non-DJ) sends.
Name an assembly (seeds + flow) and replay it later with one click. Backed by mbxhub-stations.json. GET list is summary-shaped (no seedUrls); GET one returns the full record (incl. seedUrls + timestamps); create returns the summary shape (id, name, flow, seedCount, genreQuotaExempt). All station shapes carry genreQuotaExempt (spec 2026-07-09-genre-quota-per-stream.md) — default true, so a saved station stays in its lane and skips the genre variety quota unless turned off — and filter, an optional per-station candidate filter in search-DSL syntax (e.g. year:1965..1979 -genre:Live). The station filter COMPOSES with the global autoQ.filter: global is the house rules, the station filter is the flavor, and a fill on that station must pass both. Applies to that station’s radio runs; it does not survive a MusicBee restart with the rest of the run detail.
Journey stations (spec 2026-07-25-bookended-fresh-journey) — a journey object replaces seedUrls on create: {"journey": {"waypoints": [2–5 track URLs], "fresh": true, "targetMinutes": 90}}. The first waypoint is the start and the last is the end (both pinned); any middle waypoints are pinned too. Every waypoint must have analyzed V/A mood data — an unknown or unscanned waypoint returns 400 WAYPOINT_NOT_SCANNED. Each play regenerates a fresh arc between the same bookends — a new random middle each time (when fresh, the default), sized to targetMinutes (works for long durations, hours). It queues that finite arc and does not arm the radio, so the end waypoint is the last track. GET /autoq/stations/{id} returns a journey field ({waypoints, fresh, targetMinutes}), null on a seed station.
Validation: create requires name and at least one seedUrls entry — or a journey object instead (400 INVALID_REQUEST otherwise; genreQuotaExempt is optional, defaults true). PUT is a partial update — name, genreQuotaExempt and/or filter, at least one required (400 INVALID_REQUEST otherwise). filter is validated strictly on create and update — a bad expression returns 400 INVALID_FILTER with the parse error; an empty string clears it. Unknown {id} on get/update/delete/play returns 404 NOT_FOUND.
play queues the station's seeds next, applies its flow, and starts the radio seeded (same one-action replay as RadioStart). A journey station instead queues a freshly-built finite arc between its bookends (see above) and does not arm the radio. Gated like /autoq/radio/start — party mode restricts it to the DJ, and 409 AUTOQ_DISABLED when AutoQ is off. Response: {message, name, flow, seedCount, queued}.
from-run captures the live seeded radio run (seeds, flow, and genre-exemption) as a new station in one call — the platform verb behind every surface's “Save current run as station…”. Body {"name": "..."}. 409 NO_ACTIVE_RUN when the radio is off or the run has no seed list to capture (mood runs carry no seeds). Response shape matches POST /autoq/stations.
influences is the station’s reaction memory — tracks upvoted (pick-boosted) or downvoted/banned (hard-excluded) during that station’s runs, persisted in mbxhub-station-influences.json. GET returns {stationId, upvoted:[{url, artist, title}], downvoted:[...]}; DELETE ?url= removes one track from the memory (the un-downvote verb; 404 NOT_FOUND when the track isn’t remembered, 400 INVALID_REQUEST without url). The browse station picker’s Memory button is the UI over these.
Mutation routes (POST / PUT / DELETE) return 403 FORBIDDEN when ApiReadOnlyMode is set — kiosk deployments can’t have stations mutated by remote clients.
A program is a playlist of saved stations — an ordered sequence of entries the radio moves through, one station handing off to the next after its dwell. Entries REFERENCE stations, never own them: order and dwell are the program’s own settings, while flow, candidate filter, seeds and reaction memory all still come from each entry’s station. Backed by mbxhub-programs.json. GET list is summary-shaped (no entries); GET one and create/update return the full record incl. entries (stationId, dwellTracks, dwellMinutes).
Dwell grammar: each entry sets at most one of dwellTracks (advance after N tracks played under that entry) or dwellMinutes (advance after N minutes of music); neither means hold — the radio keeps filling from that station until stopped. (The Workbench’s Programs tab labels dwell “Play for” — same values, friendlier name.) Dwell met on the last resolvable entry is a terminal hold too: the run continues as an ordinary station run rather than looping (whole-program loop is a reserved knob, no v1 behavior).
Hard boundary: the handoff re-binds the run to the next entry’s station — its flow, seeds (for centering), filter, and reaction memory — and takes effect on the next fill only; it never touches the queue, so tracks already queued from the outgoing station play out before the new station’s picks appear (eased boundaries are a reserved knob, no v1 behavior).
Validation: create requires name and at least one entry (400 INVALID_REQUEST otherwise). Every entry’s stationId must resolve to an existing station at save time, and at most one dwell may be set, positive when present — a bad entry returns 400 INVALID_ENTRY naming the failing index (e.g. entry 1: unknown stationId '...'). An entry with no dwell that isn’t last is reachable but strands every entry after it — that’s a warnings array on the 200 response, not an error (the operator may be mid-edit). PUT is a partial update — name and/or entries, at least one required; a non-null entries replaces the list wholesale (re-validated the same way, same warnings shape). Unknown {id} on get/update/delete/play returns 404 NOT_FOUND.
play is the one-action replay: starts the radio on the program’s first resolvable entry exactly like POST /autoq/stations/{id}/play, then arms program mode on top. Gated like /autoq/radio/start — party mode restricts it to the DJ, 409 AUTOQ_DISABLED when AutoQ is off, and 409 PROGRAM_EMPTY when no entry references an existing station. Play also counts as a write — it arms the radio and queues tracks — so it returns 403 FORBIDDEN when ApiReadOnlyMode is set, same as the mutation routes below. Response: {message, name, entryCount, queued}.
A playing program’s position (which entry, tracks/minutes accumulated under it) survives a MusicBee restart via the same run-detail store as the rest of the radio run — the run resumes at the same entry with dwell progress intact. If a station a program references is deleted while that program is on the air, advance skips the dangling entry (logged as a Warn) and moves to the next resolvable one; if the whole program is deleted mid-run, the run holds on its current station instead of erroring.
Mutation routes (POST / PUT / DELETE) return 403 FORBIDDEN when ApiReadOnlyMode is set — kiosk deployments can’t have programs mutated by remote clients.
Get available mood channels with arousal/valence coordinates. Channels are customizable via autoQ.moodChannels in mbxhub.json. genreQuotaExempt (spec 2026-07-09-genre-quota-per-stream.md) — default false — suspends genre-diversity quota for radio runs targeting that channel; set per channel in the mood editor at /pages/autoq.html or via PUT /autoq/settings.
// Response:
{
"success": true,
"data": {
"currentMood": "Energetic",
"channels": [
{ "name": "Energetic", "emoji": "🔥", "arousal": 0.90, "valence": 0.80, "genreQuotaExempt": false },
{ "name": "Chill", "emoji": "😌", "arousal": 0.35, "valence": 0.65, "genreQuotaExempt": false }
]
}
}
Browse tracks matching a mood channel. Returns scored tracks sorted by mood similarity. v0.5.3.3: drops federated mood-cache entries that the local MusicBee library can't resolve — peer-only URLs (replicated mood data for tracks that live on another machine) would otherwise render as “Unknown”. total reflects the filtered count, not the raw cache hits. Local-library-only display policy: federation is analytical-only.
channel | Mood channel name (e.g. "Energetic", "Chill") |
limit | Max results (default: 200, max: 500) |
// GET /autoq/moods/browse?channel=Energetic&limit=50
// Response:
{
"success": true,
"data": {
"channel": "Energetic",
"emoji": "🔥",
"total": 50,
"tracks": [
{ "url": "...", "title": "...", "artist": "...", "album": "...", "trackNo": "3", "moodMatch": 0.92 }
]
}
}
Hash-keyed counterpart to /autoq/track-mood for cross-system lookup — resolve mood data without knowing the local file URL. {hash} is hex (audioStreamSha256 = 64 chars, fileMd5 = 32 chars). Returns the same mood payload shape as /autoq/track-mood.
Raw mood data for the current track (or any track via ?url=). Returns file, album, Essentia features, percentile ranks, computed valence/arousal, and best mood channel match. When the track also has SMFM data, the response includes smfmArousal/smfmValence (0–1) — SMFM's independent read of the same mood plane, surfaced as a second source on the V/A plot. effectiveArousal/effectiveValence are always returned — the per-axis SMFM↔AutoQ blend (eff = w·smfm + (1−w)·autoq) using the weight learned from /autoq/tune/source-pair duels (see /autoq/tune/blend). They equal the pure AutoQ value until that axis has real votes AND the track has an SMFM prediction — never worse than AutoQ alone until the blend is taught.
Key and harmonic mixing. raw.key is the key root and raw.camelot its Camelot wheel code (e.g. 8A) — the pair AutoQ mixes on. keyVotes carries Essentia's three key profiles (krumhansl, temperley, edma), each {key, scale, strength}; the object appears when any vote exists and each profile is omitted individually, so read what is present rather than assuming three. The top-level key comes from edma. Do not compare the three strengths — they come from different profiles and share no scale. Use keyAgreement instead: how many of the profiles present name the key the track is actually mixed on (the flat key/mode, which comes from edma) — not the size of the largest agreeing group, which can name a key nothing is mixed in when edma is the outlier. Read it against the number of profiles the scan carried: a track may legitimately carry one or two. AutoQ weights the harmonic term by that fraction — full when they are unanimous, half on a strict majority, dropped when the mixed key is outvoted, and half again when a single profile is all that was measured. No votes at all (a pre-wave scan) counts as trusted, so existing libraries keep mixing as before.
Tonal & rhythm wave (truedat 2026-07-22, all nullable). averageLoudness (0–1, not dB and not the same scale as loudness); the tempo histogram bpmFirstPeak/bpmFirstPeakWeight/bpmSecondPeak/bpmSecondPeakWeight/bpmSecondPeakSpread (the second peak near double or half the first is genuine half/double-time evidence; a spread of exactly 0 means unmeasured); chordsKey/chordsScale (the most frequent chord, routinely different from the track key) and chordsNumberRate; the tuning block tuningFrequency (Hz, ~440 nominal but genuinely spread), tuningEqualTemperedDeviation, tuningDiatonicStrength, tuningNontemperedEnergyRatio. Absent means not measured — these are Essentia-derived and not backfillable, so only a fresh analysis fills them. hasTonalWave answers that in one field.
Chord distribution. Essentia's 24-bin chordsHistogram is deliberately not returned: its values are percentages summing to ~100 (not 0–1) and bin 0 is not C — the bin order is Essentia's internal convention, so no bin can honestly be labelled with a chord name. Two order-independent summaries are returned instead: chordsConcentration (share of the distribution in its biggest bin) and chordsEntropy (0–1, evenness). High concentration / low entropy = harmonically focused; the reverse = roaming.
Verdicts. verdicts carries truedat's write-time judgements when present: speechLikely, hiresGenuine, lossyTranscodeLikely. These re-derive on truedat's next save with no rescan, so they can change without the analysis changing. AutoQ keeps speechLikely: "yes" out of its auto-pick pool only (see autoQ.excludeSpeechTracks); tracks you queue yourself, journey waypoints and station seeds are unaffected. Truedat's own --migrate prunes catalogue entries for that verdict — it never touches audio files.
// Response (Essentia-analyzed track):
{
"success": true,
"data": {
"file": "C:\\Music\\Artist\\Album\\Track.mp3",
"album": "Album",
"valence": 0.5003,
"arousal": 0.5794,
"smfmArousal": 0.61,
"smfmValence": 0.48,
"effectiveArousal": 0.5794,
"effectiveValence": 0.5003,
"source": "essentia",
"hasEssentiaData": true,
"moodChannel": "Upbeat",
"moodEmoji": "😊",
"raw": {
"bpm": 119.95,
"mode": "major",
"loudness": -10.23,
"spectralCentroid": 1284.56,
"spectralFlux": 0.000342,
"spectralRms": 0.001245,
"spectralFlatness": 0.000089,
"danceability": 1.45,
"onsetRate": 2.87,
"zeroCrossingRate": 0.058,
"dissonance": 0.4123,
"pitchSalience": 0.3856,
"chordsChangesRate": 0.0312,
"mfcc": 142.56
},
"percentiles": {
"bpm": 0.52, "loudness": 0.61, "centroid": 0.48,
"flux": 0.35, "dance": 0.67, "onset": 0.55,
"zcr": 0.42, "rms": 0.58, "dissonance": 0.44,
"salience": 0.39, "chords": 0.51, "mfcc": 0.63
},
"confidence": 0.82,
"confidenceLabel": "high",
"genreProfile": "electronic"
}
}
// Response (fallback — no Essentia data):
{
"success": true,
"data": {
"file": "C:\\Music\\Artist\\Album\\Track.mp3",
"album": "Album",
"valence": 0.65,
"arousal": 0.70,
"source": "fallback",
"hasEssentiaData": false,
"moodChannel": "Energetic",
"moodEmoji": "🔥",
"confidence": 0.45,
"confidenceLabel": "medium",
"genreProfile": null
}
}
The speechLikely verdict above is produced by truedat (the analyser that writes mbxmoods.json), not by the hub. It draws on three independent evidence sources; none of them decides anything on its own — they surface as reasons and feed the verdict, and the verdict only gates AutoQ's auto-pick pool (see autoQ.excludeSpeechTracks). Nothing here skips a scan or removes a catalogue entry.
1. Library labels. Read from the iTunes XML, from exactly two markers: Podcast=true, or Genre equal to Podcast (exact match, case-insensitive). The old “Episode Date” heuristic was removed and must not return — MusicBee maps ID3v2.4 TDRL (a release date) into that key, so it rode on ordinary music. Publisher and long duration are not, on their own, speech.
2. Embedded file markers. A bounded header sniff (≤128 KB per file, run only over review candidates during a preview pass — never a full-library sweep), graded into three tiers rather than first-match-wins:
| Tier | Marker | Meaning |
|---|---|---|
strong | ID3 PCST / MP4 pcst | An app asserting “this is a podcast” |
provenance | WFED, TGID, purl | Came from a feed — says nothing about content. Music ships by RSS too (label feeds, session series, DJ mixes) |
genre-text | TCON exactly Podcast | Trimmed, case-insensitive, exact — not a substring, so “Comedy Podcast” or “Podcast Rock” no longer trip it |
3. Acoustic verdict (speechLikely). Computed at write time from stored Essentia features (danceability, chords strength, silence rate, zero-crossing rate, tempo-peak weight, key-vote strength), so a threshold change is retroactive across the whole catalogue with no rescan. Two gates must both fire for a “yes”: the zero-crossing signal, and danceability under 0.50. That second gate is load-bearing — sparse, live, free-form instrumental music craters on every other signal exactly like talk does, and without it genuine music (live jazz, a live rock outro) was misclassified. Measured: genuine speech sits near 0.00; real-music false positives ran 0.66–1.10.
speechLikely="yes" from its own auto-pick pool only. The only thing that keeps a file out of a scan is a rule you wrote.
Scope note: this identifies the genus — speech-dominant. It deliberately does not try to tell podcast from audiobook from lecture: that is provenance, and the audio does not carry it.
Library tracks whose chosen-source mood read falls inside a V/A box — the read-only region filter behind the AutoCal anchor-curation page (find candidates for an empty cell of the coverage grid). source=autoq uses the computed AutoQ valence/arousal; source=smfm uses the SMFM projection and skips tracks without SMFM data. Box bounds are inclusive; computes and mutates nothing.
vMin/vMax | Valence box bounds (defaults 0/1) |
aMin/aMax | Arousal box bounds (defaults 0/1) |
source | autoq (default) or smfm — which machine read to filter on |
limit | Max candidates returned (default: 50) |
// GET /autoq/autocal/candidates?vMin=0.4&vMax=0.6&aMin=0.4&aMax=0.6&source=autoq&limit=25
// Response:
{
"success": true,
"data": {
"source": "autoq",
"box": { "vMin": 0.4, "vMax": 0.6, "aMin": 0.4, "aMax": 0.6 },
"count": 2,
"candidates": [
{ "path": "C:\\Music\\Artist\\Album\\Track.mp3", "artist": "...", "title": "...", "valence": 0.52, "arousal": 0.47 }
]
}
}
Set target mood for Mood mode. Pass empty channel to disable mood mode.
// Request:
{ "channel": "Energetic" }
// Response:
{
"success": true,
"data": {
"message": "Mood set to Energetic",
"mode": "mood",
"mood": { "name": "Energetic", "emoji": "🔥", "arousal": 0.90, "valence": 0.80 }
}
}
Reload the mood channel cache from disk (re-reads Essentia data).
Export mood file (seed another node). Streams the raw mbxmoods.json with the same byte content as on disk so you can save it and drop it into another MBXHub's plugin data folder. Disabled by default. Returns 404 unless “Disable mood export” is unchecked in Settings → Configuration → Services. Also returns 404 when no moods file is resolved on this node, and 503 when the file is busy. Supports ETag + If-None-Match (weak validator) and Last-Modified for cheap re-pulls; response carries Content-Disposition: attachment; filename="mbxmoods.json".
Local mood-cache totals. Returns current cache totals (Essentia-backed and fallback). warmupInProgress is always false and lastWarmup is always null — the warm-up shape is preserved for client compatibility but no longer fires.
// Response:
{
"success": true,
"data": {
"total": 12480,
"essentia": 9812,
"fallback": 2668,
"warmupInProgress": false,
"lastWarmup": null
}
}
Bulk write mood tags to a MusicBee tag field (the native Mood field or any Custom1–16). Only processes Essentia-analyzed tracks. Requires autoQ.moodTagField to be configured (e.g. "Custom1").
The written tag is a display export for MusicBee (e.g. a customized Now Playing panel showing the field) — MBXHub writes it but never consumes it; nothing in scoring or fills reads it back. There is no automatic bulk rewrite: after anything that changes classifications (model toggle, recording markers, channel edits), run Retag from Mood Tools on the AutoQ page to bring the whole library current. Individual tracks self-heal as you go — the now-playing track's tag refreshes on every track change, and AutoQ writes the tag on each track it queues.
// Response:
{
"success": true,
"data": { "updated": 342, "elapsedMs": 1302 }
}
// When a gate refuses the run (write toggle off, circuit breaker, field
// mismatch, no mood data), the response says why instead of a silent 0:
{
"success": true,
"data": { "updated": 0, "elapsedMs": 0, "blocked": "Mood tag writing is off — pick a mood tag field in Settings → AutoQ (writeMoodTags)" }
}
Clear the configured mood tag field (the native Mood field or any Custom1–16) on every cached track that has a value. Explicit cleanup — gated on library-tags read-only only, independent of the write circuit breaker. Targets the currently configured field, so clear before switching fields. Response: { "cleared": N, "elapsedMs": N }. Resumable — re-running skips already-empty tracks.
Live progress of an in-flight retag or clear. Response: { "running": bool, "op": "retag"|"clear", "processed": N, "total": N, "affected": N }. Poll while a bulk write/clear runs to show a track count. Both ops are resumable: a terminated run is resumed by simply re-triggering (retag skips unchanged tags, clear skips already-empty), so no checkpoint is needed.
Get current vibe list (candidate tracks with scores). Query: ?count=50
// Response:
{
"success": true,
"data": {
"count": 50,
"tracks": [
{
"url": "C:\\Music\\track.mp3",
"title": "Uptown Funk",
"artist": "Mark Ronson",
"genre": "Funk",
"score": 12.5
}
]
}
}
Discover tracks adjacent to the current taste profile, grouped by category. Query: ?limit=100&groupBy=auto|genre|artist|mood
// Response:
{
"success": true,
"data": {
"profile": {
"topGenres": [{ "name": "Rock", "weight": 1.0 }],
"topArtists": [{ "name": "Foo Fighters", "weight": 0.85 }],
"bpmRange": [90, 160],
"mood": "Energetic",
"influenceCount": 3,
"reactionCount": 12
},
"groupBy": "genre",
"groups": [{
"label": "Rock",
"reason": "genre",
"count": 15,
"tracks": [{
"url": "C:\\Music\\track.mp3",
"title": "Everlong",
"artist": "Foo Fighters",
"genre": "Rock",
"score": 8.2,
"reasons": ["genre match", "artist match"]
}]
}],
"total": 45
}
}
Find tracks similar to a seed track by metadata and feature distance. Query: ?url={trackUrl}&limit=20
// Response:
{
"success": true,
"data": {
"seed": {
"url": "C:\\Music\\seed.mp3",
"title": "Everlong",
"artist": "Foo Fighters",
"genre": "Rock"
},
"tracks": [{
"url": "C:\\Music\\similar.mp3",
"title": "Learn to Fly",
"artist": "Foo Fighters",
"genre": "Rock",
"similarity": 0.6,
"reasons": ["same genre", "same artist", "similar BPM"]
}]
}
}
Tiered reactions for the now playing track. Each emoji has a different score weight.
| Emoji | Name | Score | Description |
|---|---|---|---|
| 🔥 | fire | +3 | This track is fire! (triggers queue refresh) |
| ❤️ | heart | +2 | Love this song |
| 👍 | like | +1 | Good choice |
| 👎 | dislike | -1 | Not feeling it |
| 🚫 | ban | -100 | Skip and exclude from queue |
Submit a reaction for a track. Reactions appear as floating emojis on the Display page. While the radio runs, a reaction to a scanned track also nudges the run’s center — positive pulls 20% toward that track’s mood point, negative pushes 10% away; persisted with the run detail.
// Request body (always reacts to currently playing track):
{
"emoji": "🔥",
"nickname": "Mike"
}
// emoji: "🔥", "❤️", "👍", "👎", "🚫" or "fire", "heart", "like", "dislike", "ban"
// Response:
{
"success": true,
"data": {
"recorded": true,
"emoji": "🔥",
"trackUrl": "C:\\Music\\track.mp3",
"trackTitle": "Uptown Funk",
"trackArtist": "Mark Ronson",
"nickname": "Mike"
}
}
Get reaction history. Query: ?trackUrl= for specific track, ?limit=100
// Response:
{
"success": true,
"data": {
"count": 25,
"trackUrl": null,
"reactions": [
{
"emoji": "🔥",
"type": "fire",
"trackUrl": "C:\\Music\\track.mp3",
"trackTitle": "Uptown Funk",
"trackArtist": "Mark Ronson",
"nickname": "Haro",
"timestamp": "2026-02-01T20:45:00Z"
}
]
}
}
Get party leaderboard data: guest activity, top tracks, reaction breakdown.
// Response:
{
"success": true,
"data": {
"totalReactions": 156,
"topTracks": [
{ "url": "...", "title": "Uptown Funk", "artist": "Mark Ronson", "score": 15 }
],
"guests": [
{ "nickname": "Haro", "totalReactions": 42, "fire": 10, "heart": 15, "like": 12, "dislike": 3, "ban": 2 }
]
}
}
Get all tunable AutoQ parameters: scoring weights, reaction scores, influence scores, estimation weights, and normalization ranges. Use with Tuning Console.
Partial update of AutoQ parameters. Only provided fields are changed. Changes take effect on the next scoring pass — no restart needed. Nested objects are merged, not replaced.
The tightness pair and slide pulls are settable here as flat fields: funnelReach01 (Reach) and glideTightness (Continuity), both 0–1, plus slideEvery (0–50, 0 = off) and slideReach (0–1). These are the stored defaults behind the radio strip's Tight↔Loose dial, which sets the pair live for the current run via POST /autoq/radio/tightness. Out-of-range values are ignored rather than clamped — the field simply does not appear in the response's updated list. excludeSpeechTracks (bool) and maxTrackLengthSeconds (int seconds, 0 = off) are settable here as well. Editing Reach or Continuity while the radio is running applies to that run from the next fill: the run normally derives both from the dial, so a hand edit would otherwise be inaudible until the next start. Moving the dial afterwards drops the override.
// Example: change just the BPM arousal weight
PUT /autoq/settings
{"estimation": {"arousalWeightBpm": 0.30}}
AutoQ settings in mbxhub.json under the autoQ section:
| Setting | Default | Description |
|---|---|---|
enabled | false | Enable AutoQ feature |
autoHeart.enabled | false | Auto-Heart: a Fire/Heart reaction on the now-playing track loves it in your MusicBee library (solo mode only). Off by default; opt-in. Respects the Loved feature toggle (apiDisableLoved) and the read-only tag flags; independent of protectMetadata. |
pickMode | "weighted" | How AutoQ selects candidates: off (AutoQ stops picking — TrueShuffle fills instead, if enabled), favorites (always pick highest-scored), weighted (score-proportional random from top candidates), random (uniform random, diversity caps still apply), fresh (play-recency: least-recently-played first, never-played is freshest). The effective queue driver is derived from this plus the AutoQ/TrueShuffle enable flags (AutoQ when enabled & pickMode ≠ off, else TrueShuffle if enabled, else Off) — surfaced in the MusicBee Settings dialog and the AutoQ Workbench so the active mode is unambiguous. |
freshStrict | false | Applies to fresh pick mode. true = strict freshest-first (never-played, then oldest-heard); false (default) = freshness-weighted random that still favors fresh tracks. Never-played composes with the playcount:0 filter, not a separate control. |
freshness | "Free" | Global default freshness for AutoQ picks: Free (no recency constraint), Fresh (least-recently-played first), NeverHeard (only 0-play tracks). A saved station's own freshness overrides this (unset = inherit global). NeverHeard composes with the playcount:0 gate; Fresh reuses the fresh pick-mode recency. Surfaced as the Freshness control (Never heard / Fresh / Free) in the AutoQ Workbench Builder's Filters section, and baked into a station by Save-as-station. |
queueThreshold | 3 | Add tracks when queue drops below this; also the seed window — the nearest this-many queued-ahead tracks seed a seeded radio start |
batchSize | 5 | Tracks to add per batch |
maxTrackLengthSeconds | 0 | Longest track AutoQ will pick, in seconds (0 = no limit). Explicitly queued tracks are unaffected. Unknown-duration tracks are excluded while strictDurationLimit is on (the default), kept when off. |
excludeSpeechTracks | true | Keep talk / podcast tracks out of AutoQ's auto-pick pool. A candidate whose Truedat verdict is speechLikely="yes" is dropped at the same candidate choke point as maxTrackLengthSeconds, so fills, generate, and journeys all inherit it; explicitly queued tracks, journey waypoints, and station seeds are unaffected. Only "yes" excludes — missing verdict, "unknown", "n/a", and "no" all stay playable, so coverage fills in gradually as the library is scanned. On by default; turn off if the (untuned) speech classifier shows early false positives. |
filter | "" | Candidate filter in search-DSL syntax. Works as include and exclude: a plain qualifier keeps only matches, a - prefix drops matches; terms AND together, OR and parentheses compose. Examples: include genre:rock, year:1980..1989, rating:>=4, genre:rock rating:>=3; exclude -genre:metal, -year:1990..1999, -genre:"Test Disc" -year:1990..1999 duration:<1200; compound (genre:rock OR genre:punk) -artist:"Nickelback" duration:<600. Empty = off. Saved strictly (PUT /system/config returns 400 INVALID_FILTER on a bad expression). Filterable keys: genre, year, duration, bpm, rating, playcount, artist, album. Missing data never matches a predicate (an include drops it, an exclude keeps it); unknown duration is excluded while strictDurationLimit is on (the default), kept when off. Explicitly queued tracks, journey waypoints, and station seeds are unaffected. A filter gates new fills only — it does not sweep tracks already in the upcoming queue; clear the queue to apply it right away. |
strictDurationLimit | true | Treat duration limits as hard gates. A track whose duration is unknown (unscanned, 0) can't be proven within a bound; on (the default) it is excluded by both a duration: filter bound and maxTrackLengthSeconds, so a limit means a limit; off it is kept (lenient). Only bites when a duration limit is set. Explicitly queued tracks, journey waypoints, and station seeds bypass either way. |
glideTightness | 0.6 | PFS glide / Continuity: weight of flow-from-previous (acoustic timbre + Camelot key + half/double tempo, now read from the tempo histogram's second peak where the scan found one + chord density) vs the flow target in each pick (0-1). Surfaced as the Workbench Mood tab’s and the AutoQ console’s Continuity slider. Off-air this stored value is the glide. On-air the radio strip Tight↔Loose dial governs — the glide is derived from the dial and this field is left untouched (live per-run via POST /autoq/radio/tightness) — until you edit the slider, which overrides the dial for that run from the next fill on; moving the dial afterwards hands control back to it. The harmonic (Camelot) part of the term is scaled by keyAgreement, read as n-of-m over the key profiles a scan actually carried — full when two or more agree on the mixed key, half on a strict majority or a lone profile, dropped when the mixed key is outvoted; entries with no votes (pre-2026-07-22 scans) count as trusted. |
continuityKeyWeight | 0.25 | Key weight: how much of the continuity blend is Camelot key compatibility, relative to the fixed mood 0.30 / timbre 0.20 / tempo 0.25 weights (the sum renormalises). Raise for fewer key clashes in the ordered chain at the cost of mood/tempo smoothness; 0 removes the key term entirely. Surfaced as the Workbench Mood tab’s and the AutoQ console’s Key weight slider. Applies to radio fills, generate, and journeys alike. |
funnelReach01 | 0.246 | Reach: how wide the candidate funnel opens (0 = tight/near the run center, 1 = wide-roaming). Surfaced as the Workbench Mood tab’s and the AutoQ console’s Reach slider. Same override rule as glideTightness: the strip Tight↔Loose dial governs a run by default and this field is left untouched; editing the slider overrides the dial for that run from the next fill on, and moving the dial clears the override. Applies to radio fills, generate, and journeys alike. |
slideEvery | 10 | PFS slide: every Kth pick steps outside the neighborhood (0 = off) |
slideReach | 0.35 | PFS slide: minimum mood-space distance from the previous track for a slide pick (0-1) |
autoq-flows.json | — | Drop-in flows (file beside mbxhub.json, not a setting): {"flows":[{"name":"gentle","kind":"wave","waveDepth":0.1},{"name":"sunrise","curve":[0.2,0.5,0.9]}]}. Named flows = base kind + knob overrides, or an absolute arousal curve lerped across each batch. Usable everywhere a flow name is accepted (radio start, generate/connect, saved stations, the Workbench picker); hand-edits live on next use; built-in names can’t be shadowed; deleted customs degrade to smooth. |
vibeListSize | 100 | Size of candidate track pool |
moodMatchWeight | 0.4 | Weight for mood matching in scoring (0-1) |
recencyDecayLambda | 0.1 | Decay rate for recency boost on reacted tracks |
recencyPenaltyDecay | 0.1 | Decay rate for recently-played penalty |
minReplayMinutes | 30 | Minimum minutes before a track can be replayed |
diversityWindowSize | 10 | Recent tracks considered for diversity calculations |
minSessionEntropy | 0.5 | Entropy threshold before boosting diversity (0-5) |
vibeListRefreshMinutes | 30 | Minutes between automatic vibe list refreshes |
moodChannels | null | Custom mood channels (array). Uses defaults if null. Each channel carries its own genreQuotaExempt (default false) — spec 2026-07-09-genre-quota-per-stream.md. |
genreQuota | 3 | Max consecutive same-genre tracks (0=disabled). Suspended per-run when the run is genre-quota exempt (saved stations default exempt, plain seeded starts default exempt, mood channels default not-exempt) — spec 2026-07-09-genre-quota-per-stream.md. |
artistQuota | 1 | Max tracks from same artist in batch (0=disabled) |
moodTagField | "Custom1" | MusicBee tag field for mood labels — the native Mood field or any Custom1–16 (null=disabled) |
moodTagFieldName | "AutoQ Mood" | Expected display name for the tag field (must match MusicBee config) |
sendMode | "refresh" | Default Send to Q Behavior: reuse (join seeds cumulatively), refresh (newest send leads, weighted), restart (replace seeds outright) |
sendRefreshWeight | 0.7 | Refresh weighting 0–1: how strongly the newest send dominates the recomputed run center (0 behaves like reuse, 1 like restart) |
sendWaypointCap | 3 | Max waypoints one send adds to an in-flight journey (1–4; engine hard cap is 4 plus the destination = 5 pinned) |
sendQueueCap | 0 | How many sent tracks queue immediately (0 = all). Steering always uses the full batch regardless of this cap |
sendBoostStrength | 1.0 | Station-memory write per send: 0 disables, above 0 records an upvote (the store is binary today — parity with a thumbs-up) |
sendIdleStart | "station" | Idle send behavior: station (start one from the sent tracks) or tray (collect only, surface-side) |
sendMaxSeeds | 12 | Seed-set bound for cumulative modes; the oldest send generation evicts first, the newest always survives |
sendMagnetPolicy | "keep" | keep = active mood magnet survives a send; clear = a send drops it so a re-anchor is not fought by a stale magnet |
sendWaypointOverflow | "queue" | Sent tracks beyond the waypoint cap: queue (play but do not bend the arc) or drop |
sendQueuePlacement | "next" | Where sent tracks land in the queue: next or end |
sendRefill | "keep" | keep = already-queued fills stay; replace = unplayed fills behind the sends are re-picked against the new center |
Send to AutoQ (POST /autoq/send, above): sendMode, sendRefreshWeight, and sendWaypointCap are the three primaries surfaced on the generic settings page; the rest are secondary knobs (workbench + /autoq/settings only) whose defaults reproduce the ruled behavior exactly.
Scoring weights under autoQ.scoringWeights: featureSimilarity (0.5), trackSentiment (0.25), artistSentiment (0.15), recencyPenalty (0.3), diversityPenalty (0.6), explorationBonus (0.1), influenceWeight (0.3).
Reaction scores under autoQ.reactionScores: fire (3), heart (2), like (1), dislike (-1), ban (-100).
Estimation, valence/arousal weights, normalization ranges, confidence thresholds, and genre profiles — full field list with defaults and descriptions is in the config reference. Live values: GET /autoq/settings. Tune interactively at /pages/autoq.html.
Mood Quadrants — Arousal (energy) is the vertical axis, valence (positivity) is the horizontal. Each mood channel targets a point in this space.
| Quadrant | Profile | Typical Genres | Acoustic Traits |
|---|---|---|---|
| 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 |
VAM (Valence / Arousal / Mood) is the lens AutoQ uses to score tracks. The calibration loop centers on Auto-Cal, a MusicBee playlist the user maintains by hand. It holds anchor tracks — songs picked as corner / archetype examples for the V/A axes (“this is maximum energy,” “this is deepest sad”). Everything in this section either feeds Auto-Cal, reads from it, or measures how well the current model agrees with it.
End-to-end:
vamAnchorPlaylist setting (default "Auto-Cal").GET /vam/gates/report. The Gates panel at the top of /pages/autoq.html joins the playlist with your annotations and shows per-track and overall fit between the current model and your taste (residual, strict-pass count, fail count, mean residual).tools/ (calibrate-valence-arousal.py + extract-sony-sensme.py) plus a requirements.txt for reproducibility — developer-use only. The user-facing retrainer is planned to move into the Truedat repo so it ships next to the analyser that produces mbxmoods.json.POST /vam/model/reload. The loader prefers %AppData%\MBXHub\mood-model.json over the embedded copy in the DLL, so a fresh retrain swaps in without a rebuild.The pairwise tuning page (/pages/tune.html) feeds the same calibrator from the other side — A/B comparisons accumulate as Bradley-Terry signals that fit alongside the anchor labels.
The annotate page reaches the rating card via the random quadrant-balanced queue, or a deliberate search-and-pick of a known corner song to use as an anchor.
Pick the next track for the rating card: a honeypot re-serve of an already-rated track about 10% of the time (once at least 5 are rated), else the next unrated Auto-Cal anchor if one remains, else a fresh quadrant-balanced pick from the library. Query param mode=disagreement switches the fresh-pick step to target the unrated essentia+SMFM track where AutoQ and SMFM diverge most (valence weighted 4:1 over arousal, rarest-quadrant tiebreak within a 0.05 score band); silently falls back to the standard pick when no SMFM-bearing unrated candidates remain. Honeypot and Auto-Cal anchor priority are unchanged by the mode. Card shape: {key, url, title, artist, album, isRetest, isAutoCal, rated, modelV, modelA, smfmV, smfmA} — modelV/modelA are the current model's prediction (opt-in pre-fill ghost on the plot), smfmV/smfmA are SMFM's projected second opinion (nullable, 4dp, null when the track has no SMFM). 503 NO_LIBRARY if the library has no tracks; 503 NO_ESSENTIA_DATA / 503 NO_TRACK if no unrated essentia-scanned candidate exists.
Search the library for a track to pick. Query param q — case-insensitive substring match on title + artist, filtered to essentia-scanned tracks with an audioStreamSha256 key. Returns up to 20 results; q shorter than 2 chars returns an empty list. rated is true for tracks already labelled.
GET /vam/annotate/search?q=miles
{"success":true,"data":{"results":[
{"key":"<sha256>","url":"file://...","title":"So What",
"artist":"Miles Davis","album":"Kind of Blue","rated":false}
]}}
Load a specific chosen track as a rating card. Query param url — must be in the library pool and essentia-scanned. Returns the same card shape as /vam/annotate/next. 404 NOT_PICKABLE if the url is unknown or not essentia-scanned. Picked tracks default the anchor flag on — picking is the deliberate-anchor path.
The rating endpoint is the one that mutates — everything else here is read-only. POST /vam/annotate/rating honors ApiReadOnlyMode (returns 403 API_READ_ONLY) so kiosk deployments can't accumulate stray labels.
Persist a V/A rating for one track. Body {key, valence, arousal, confidence ("sure"|"unsure"), isAnchor, path}. Tracks in the configured vamAnchorPlaylist get isAnchor=true forced server-side, so the user can't accidentally unset the anchor flag on a curated calibration track. Response: {ok, total, file}.
Anchor editor: create or OVERWRITE a label's primary V/A by clicking the plot. Body {key, valence, arousal, confidence?, isAnchor?, path?} (isAnchor defaults true — a placement is a deliberate anchor). Unlike /rating it overwrites the primary and does NOT append a retest; training input only, never overrides the deterministic model output. Honors ApiReadOnlyMode. Response: {ok, updated, total, file}.
Anchor editor: delete a label. Body {key}. Honors ApiReadOnlyMode. Response: {ok, removed, total, file}.
Counter snapshot for the annotation HUD: {rated, librarySize, remaining, anchors, unsure, quadrants:{q1angry, q2happy, q3sad, q4calm}, selfConsistency:{retested, meanValenceDelta, meanArousalDelta, score}}. selfConsistency.score is 1 - mean(|ΔV|, |ΔA|) across honeypot retests; null until at least one retest has happened.
Reference markers for the V/A scatter plot: the 12 fixed AutoQ mood channels ({name, emoji, valence, arousal}) plus the user's own anchor tracks ({key, valence, arousal, path}).
Full labels store in the shape the Phase V2 trainer consumes: {count, file, labels:[{key, valence, arousal, confidence, isAnchor, path, ratedAt, sessionId, retest:[…]}]}. The trainer reads this file via --labels; the endpoint is the canonical shape.
Coarse gross-error gate (spec §4). Compares each anchor's user-rated quadrant against a producer's V/A prediction with a centre dead-zone so a near-neutral track doesn't fail for crossing a line by a hair. Default producer is mbxhub-formula (in-process). Query params: ?predictions=<path> points at an external V2 producer JSON, ?deadZone=0.15 overrides the default tolerance. 503 NO_ANCHORS if no anchors are designated yet.
Compare the embedded model's predictions against the user's anchor labels. The Gates panel at the top of /pages/autoq.html consumes this surface, and it's the sanity check after every retrain.
Enumerate the vamAnchorPlaylist (default "Auto-Cal") and join each track with its VAM anchor annotation. Returns per-track rows (label V/A, model V/A, residual, pass/fail vs configured tolerance) plus summary stats (strict pass count, fail count, mean residual). Resolved by playlist name on every request — rename in MusicBee and the endpoint reflects it without restart.
Coordinate-descent fit of the legacy formula weights against the rated anchors. Returns suggested values for every valenceWeight* / arousalWeight* / intercept. Only takes effect at scoring time when useTrainedModel=false — otherwise the formula path is bypassed and the Tuning Console badges this state.
Same fitting machinery as /vam/gates/suggest-weights; returns the full calibration response shape (initial / final RMSE, per-weight delta). Same caveat re useTrainedModel.
The trained model ships embedded in the DLL but a disk copy at %AppData%\MBXHub\mood-model.json supersedes it. Lets you iterate on retrained models without rebuilding: train → write to AppData → POST /vam/model/reload → loader picks up the new file.
Returns {loaded, source ("disk"|"embedded"|"none"), overridePath, useTrainedModel, active, activePath ("model"|"formula"), model:{version, trainedAt, anchorsUsed, featureCount, valenceLoocvR, arousalLoocvR, essentiaOnlyHead, essentiaOnlyValenceR, essentiaOnlyArousalR}}. Drives the status pill + formula-hidden state at the top of the AutoQ Tuning Console's Estimation Mixer. essentiaOnlyHead is a boolean; essentiaOnlyValenceR / essentiaOnlyArousalR are the fallback head's LOO-CV r (null when no head is loaded). When present, tracks with no SMFM are scored by an essentia-only head instead of the fused model, so a low-SMFM library degrades gracefully.
Drop the cached model, re-read from disk (override wins if file exists) or the embedded resource, then recompute every cached per-track valence/arousal in place — the promoted model takes effect immediately, no MusicBee restart. Returns the same shape as /vam/model/info plus recomputed (entries swept; -1 when the mood cache wasn't loaded yet). Honors ApiReadOnlyMode (returns 403 API_READ_ONLY).
The live-tuning corrections currently layered on top of the loaded model. Rating a track on the annotation page stores a base-relative correction (rated − model) that shifts its served valence/arousal immediately, kept in mood-residuals.json keyed by audioStreamSha256 — resettable, changing no files of record. Returns {count, file, entries:[{key, corrV, corrA, ratedAt}]}. Read-only; empty when nothing is tuned.
Reset level 1: drop every live-tuning correction, returning pure loaded-model output. Non-destructive — leaves the labels (mbxvam-labels.json) and the model file untouched. Returns {ok, cleared} (count dropped). Honors ApiReadOnlyMode (403 API_READ_ONLY).
Training-data export consumed by rebuild-mood-model.cmd / tools/calibrate-valence-arousal.py: a sha-keyed derived view of the in-memory mood cache carrying every trainer feature plus the C#-only modeMajor encoding (1.0 major / 0.0 minor / null unknown) and the projected smfmValence/smfmArousal second opinion. Raw JSON, not the success envelope — streamed directly to the response, roughly 10–40 MB at 70K tracks. Shape: {generatedAt, projectorVersion, projectorHash, tracks:{<audioStreamSha256>:{path, audioStreamSha256, …essentia features…, modeMajor, smfmValence, smfmArousal}}, count, skippedNoSha}. Entries without an audioStreamSha256 are skipped and counted in skippedNoSha (labels join by sha). mbxmoods.json itself is never modified — this is a read-only derived view.
Read-only observability for VAM tuning: the active model + its LOO metrics, which training inputs exist and their counts (mbxmoods / mbxvam-labels / mbxtune-pairs) plus mood-cache stats, the active-vs-suggest-weights distinction, a bakeEnv block ({pythonOnPath, pythonPath} — cheap PATH scan for a future retrain/bake; no process spawn or sklearn probe), and a knownGaps block flagging documented model-recording gaps (e.g. tune-pair usage not recorded in the model file; trainer joins labels to features by path, not audioStreamSha256). Answers "what is the model actually consuming?" Computes and mutates nothing.
The tuning page (/pages/tune.html) is the "second opinion" loop for V/A scoring. The user picks the higher-on-axis track from a pair (or marks them roughly equal); judgments accumulate as Bradley-Terry signals that the V2 trainer fits alongside the single-track anchor labels. Pair selection biases toward under-compared tracks and close-pair candidates — the model is most uncertain when scores are similar, so each judgment carries more signal than a random pick across 71k tracks where most pairs are trivially separable.
Build a fresh A/B pair. Query param axis = arousal or valence. Picks A as the least-compared candidate (random tie-break), then picks B as the candidate whose model score is closest to A. Response: {axis, a:{url, title, artist, score, comparisons}, b:{url, title, artist, score, comparisons}}. 503 NO_LIBRARY if the library has fewer than 2 essentia-scanned tracks.
Persist a pairwise judgment. Body {a, b, axis ("arousal"|"valence"), verdict ("a"|"b"|"tie")}. Append-only to mbxtune-pairs.json in the AppData folder. Honors ApiReadOnlyMode (returns 403 API_READ_ONLY). Response: {ok, total, axis, file}.
Per-track Bradley-Terry scores fitted from the accumulated judgment store. Query param axis. Response: {axis, pairCount, trackCount, tracks:[{url, score, comparisons}]}. Used by the tune page to show the score distribution + outliers.
File a misclassification flag against a single track. Body {url, observedChannel?, observedArousal?, observedValence?, expectedChannel?, notes?}. Separate store from judgments (mbxtune-flags.json); the trainer uses these as bias-correction signals. Honors ApiReadOnlyMode (returns 403 API_READ_ONLY). 503 FLAG_STORE_DISABLED if the optional flag store isn't wired. Response: {ok, total, file}.
All flags, or per-track aggregates with ?summary=true. Summary mode returns {trackCount, totalFlags, tracks:[…]}.
SMFM-vs-AutoQ source duel. Samples ONE high-divergence track that has BOTH an SMFM and an AutoQ V/A prediction — the largest gap between the two sources is where a human vote is most informative. Response: {url, title, artist, album, smfm:{arousal,valence}, autoq:{arousal,valence}, gap:{arousal,valence}, duelsSoFar, sampledCandidates}. 503 NO_SOURCE_PAIR when no track has both sources (needs a truedat SMFM scan + essentia mood data); 503 NO_LIBRARY when the library is empty.
Persist a source-duel vote — which source's V/A is closer for that track, per axis. Body {url, axis ("arousal"|"valence"), winner ("smfm"|"autoq"|"same"|"skip")}. Append-only to mbxtune-source-duels.json (separate from the track Bradley-Terry store). Response: {ok, total, axis, file}. Errors: 403 API_READ_ONLY, 503 SOURCE_STORE_DISABLED, 400 INVALID_FIELD (bad axis/winner), 400 BAD_JSON / EMPTY_BODY.
The per-axis SMFM blend weight learned from the duels. Response: {arousal:{wSmfm, n}, valence:{wSmfm, n}, totalDuels}. wSmfm in (0,1) is how much the effective V/A leans on SMFM vs AutoQ for that axis (0.5 = neutral / no votes yet, Laplace-smoothed). This is the weight /autoq/track-mood's effectiveArousal/effectiveValence consume. 503 SOURCE_STORE_DISABLED when the store isn't wired.
Generic seam for computing and writing MusicBee Custom* fields from registered providers. Master flag: customFields.enabled (default false). When off, all /fields/* endpoints return 403 FIELDS_DISABLED. Enable from the Labs category in Settings; the Labs page (/pages/labs.html) is linked from the Configuration page.
First provider — SMFM (smfm): reads the 10 raw SMFM/STMO scores captured from the 12 TONE/SMFM block during the Truedat scan (stored in mbxmoods.json), projects them to an (arousal, valence) point on the same mood plane AutoQ uses, and writes those two scalars (0–1) to Custom* fields. SMFM is a second source on the AutoQ V/A plot — the same kind of signal as AutoQ's Essentia-derived valence/arousal, from a different origin. The raw 10 scores + BPM are kept as-is.
Matching: a track resolves to its SMFM data by absolute path, falling back to its path tail (artist\album\file.ext) when the path misses — so a mbxmoods.json scanned under a different drive root or category folder still matches the local library.
Settings (mbxhub.json): customFields.enabled (bool, default false, master flag); customFields.smfm.enabled (bool, default false); customFields.smfm.retagOnReload (bool, default false); customFields.smfm.fields (array of {field, name} — two targets, default [{"field":"Custom2","name":"SMFM Arousal"}, {"field":"Custom3","name":"SMFM Valence"}]).
Retag triggers: (1) manual POST to /fields/{provider}/retag; (2) PUT to /fields/{provider}/config (settings change queues a background retag automatically).
Stale-field gotcha: a retag writes only the currently-configured fields. Repointing Arousal or Valence to a different Custom* field leaves the old field holding a stale value — there is no auto-clear. Clear old fields manually before reconfiguring.
List all registered providers. Response: {providers:[{id, displayName, enabled, targets:[{field,name}], coverage:{total,withData}}]}. total is library size; withData is tracks that have source data (e.g. the raw 10 SMFM/STMO scores for smfm). Returns 403 FIELDS_DISABLED when customFields.enabled is false.
Coverage detail for one provider. Response: {provider, total, withData, arousal:{mean}, valence:{mean}} — total is library size, withData is the count of tracks with an SMFM (arousal, valence), and arousal.mean/valence.mean summarize where the library sits on the V/A plane. 404 NO_PROVIDER for unknown provider ids. 403 FIELDS_DISABLED when facility off.
SMFM mood + raw scores for one track. ?url= is the file path (UTF-8). Response: {url, found, arousal, valence, scores} — arousal/valence are the projected (0–1) mood position, scores the raw 10-element SMFM/STMO array. found:false with arousal/valence null and an empty scores array when the track has no SMFM data. 400 MISSING_URL if ?url= is omitted.
Update provider settings and queue a background retag. Body: {fields?:[{field,name},...]} — the two target Custom* fields (Arousal, Valence). Persists accepted values to mbxhub.json and queues a background retag. Response: {result:true, provider, retagQueued:true}. Errors: 400 INVALID_REQUEST (missing body or empty field/name), 400 INVALID_FIELD (field not Custom*), 400 UNSUPPORTED (provider ≠ smfm), 403 READ_ONLY, 404 NO_PROVIDER, 500 SAVE_FAILED.
Bulk-write configured fields for all tracks that have source data. Blocking (completes before response). Response: {updated, elapsedMs}. If a retag is already running returns immediately with {updated:0, elapsedMs:0, skipped:"in-progress"}. Errors: 403 READ_ONLY, 404 NO_PROVIDER, 500 RETAG_FAILED. Only writes currently-configured fields — see stale-field gotcha above.
The undo for retag: wipe the provider’s configured target fields on every candidate track in one pass (both SMFM slots at once — no per-field switcheroo). Blocking. Response: {cleared, elapsedMs}; {cleared:0, elapsedMs:0, skipped:"in-progress"} when a retag/clear is already running. Errors: 403 READ_ONLY, 404 NO_PROVIDER, 500 CLEAR_FAILED. The Labs page’s Clear button (next to Retag Now) is the UI over this. Targets the currently configured fields — clear before switching fields.
List review manifests dropped into <AppData>\MBXHub\review\ by offline tools (tools/fleet/). Each entry: {id, kind, title, generated, classCount, hasVerdicts, savedUtc}. The id is the file's name stem — drop each manifest as <id>.json (that is what /review/manifest/{id} and the verdicts routes resolve); an internal id property, if present, is display-only and ignored for routing. The hub never computes diffs — it serves manifests and stores verdicts. Unreadable manifest files are skipped with a warning in the log.
Full manifest JSON. Classes are self-describing — each carries its own columns, rows (or rollup for huge classes), rulingOptions, and rowOptions, so new scenario kinds (diff, identity, coherence, dupes) need no API or page changes. Errors: 400 REVIEW_INVALID (bad id), 404 REVIEW_NOT_FOUND.
Operator winner rulings, stored beside the manifest as {id}.verdicts.json. POST body: {id, rulings:{<classKey>:{ruling, overrides:{rel:value}, excludes:[folder]}}}; the hub stamps savedUtc (authoritative) and keeps the prior file as a single .bak (atomic replace). GET returns the saved verdicts or 404 REVIEW_NO_VERDICTS. POST returns 403 API_READ_ONLY when ApiReadOnlyMode is set. Other errors: 400 REVIEW_INVALID (bad id, non-JSON body, missing rulings, or body id mismatch), 404 REVIEW_NOT_FOUND (no such manifest), 413 REVIEW_TOO_LARGE (> 4 MB — verdicts carry rulings, never row data). Review page: /pages/review.html renders any manifest generically; the same renderer ships in the standalone offline door emitted by tools/fleet/build-review-manifest.ps1 -Standalone.
Serve the interactive companion page a manifest points at via source.reviewHtml (e.g. truedat's co-emitted dupes.html), same-origin as text/html — a plain file:// link from the http page is browser-blocked. Only the manifest's own declared reviewHtml is served, constrained to a bare .html filename resolved strictly inside the review folder (no path component, no traversal). 404 REVIEW_NO_ASSET when the manifest declares no companion. The /pages/review.html view shows an “Open interactive review” link when this is present.
Apply a decisions delta by delegating to truedat --apply-exclusions. The hub writes the delta to a temp file unparsed and invokes truedat — truedat owns the merge and is the only writer of the exclusion file; the hub never writes it. On success (200) the response body is truedat's apply-result.json raw ({kind, ok, added, removed, alreadyPresent, notFound, changed, backupPath, error}) — read ok/added/removed at the top level; this endpoint is a passthrough shim, not the hub envelope. Rule kinds are the closed set folder/genre/file. Errors use {error:{code, message}}: 403 API_READ_ONLY (read-only mode), 400 REVIEW_INVALID (bad id or missing source.exclusionsPath), 404 REVIEW_NOT_FOUND (no such manifest), 409 REVIEW_TOOL_BUSY (truedat already running — single slot), 413 REVIEW_TOO_LARGE (body too large). This is a scan-cost exclusion — it stops truedat spending analysis time on the track and does not remove it from AutoQ picking. To keep a track out of AutoQ, use POST /banlist (reference §9a).
Manifest kinds. The page renders any manifest from its declared columns/options. kind:"dupes" gets a group-keeper affordance: member rows are grouped by duplicate set, each group shows its copies with the recommended keeper marked (a rec badge) and a radio to pick one. Display-only — Save is hidden and no verdicts are written. The dupes manifest is emitted directly by truedat --duplicates --manifest <path> (dropped as <AppData>\MBXHub\review\dupes.json); diff and other kinds come from tools/fleet/ producers.
Scanning is how tracks earn their intelligence. truedat — MBXHub’s companion audio analyser — reads each file’s actual audio and writes the results to mbxmoods.json: mood position (valence/arousal), key and Camelot code, tempo detail, timbre, and the write-time verdicts (speechLikely, hiresGenuine, lossyTranscodeLikely). The hub consumes that catalogue live — everything from AutoQ’s picks to key: search to the Camelot wheel reads it. No tags are written to your files; re-tuning a threshold re-derives verdicts across the whole catalogue with no rescan.
The hub launches and supervises external tools through a general-purpose Tool Runner, so a scan runs from a button instead of a stray console: tool discovery on your system, one-instance-at-a-time launching (a second start is refused with already running, never stacked), running/finished status with exit code and duration, and output capture into the hub’s log for hidden runs. It never kills a process — the hub shutting down leaves a scan running to completion. truedat is the first tool to ride it; nothing in the runner is truedat-specific.
Quick start. 1 — Drop truedat next to the plugin (Plugins\truedat) or anywhere on PATH, and turn on truedat.enabled; a blank truedat.path auto-discovers and writes back the found location. 2 — Open the AutoQ Workbench → Mood tab → Scan. 3 — Let it run (a visible console by default; hidden routes output to the hub’s log instead). 4 — Results land in mbxmoods.json and the hub picks them up live — no restart. Re-run any time: already-analysed tracks are skipped, so follow-up scans only cost the new files.
Running a scan. The Scan button on the AutoQ Workbench’s Mood tab launches truedat against your library (discovery + prerequisites handled; the button explains any refusal). ARiA’s run(truedat) uses the same supervised path when the truedat.ariaWhitelist mirror is on. Behaviour is governed by the truedat.* settings — enabled, path, args, hidden, and pauseOnExit (keep the console open on “Press any key…” after the run so the output can be read; the one-scan slot stays held until the window is dismissed). See the settings reference for the full table.
What a scan is not. POST /aria/scan (ARiA section) triggers MusicBee’s own scan-folders-for-new-files — it finds new files; it does not analyse audio. Scan-cost exclusions (POST /review/decisions/{id}, Review section) keep truedat from spending analysis time on a track — they do not affect AutoQ picking (that is POST /banlist). The data a scan yields is read back per-track via GET /autoq/track-mood (AutoQ section).
Simulate keyboard and mouse input to wake or control the host PC. Useful for remote wake scenarios. Full ARiA Documentation →
ariaEnabled: false). Returns 403 ARIA_DISABLED when disabled.
Check if ARiA input simulation is enabled
Quick wake: move mouse + send Shift key to wake sleeping/locked PC
Trigger a MusicBee library scan (built-in, like Wake PC). Sends Insert to open the scan-folders dialog, then a focus-skipped Enter to confirm it. Assumes Insert is bound to scan-folders (MusicBee default).
Send keyboard input. Body: {"keys": "^a"} (Ctrl+A). Optional: {"keys": "%{F4}", "window": "Notepad"} to focus window first. Prefix keys with ! to send to the current foreground window without refocusing MusicBee.
SendKeys format: ^=Ctrl, %=Alt, +=Shift. Special keys: {ENTER}, {TAB}, {ESC}, {F1}-{F12}, {UP}, {DOWN}, etc.
DuckyScript format: CTRL ALT V, SHIFT F1, ALT TAB. Modifiers: CTRL, ALT, SHIFT. Special: WIN/GUI (opens Start Menu, standalone only - not a modifier).
Focus a window by title. Body: {"window": "Notepad"} (partial match, case-insensitive)
Move: {"x":100,"y":100} (absolute) or {"dx":10,"dy":0} (relative)
Click: {"button":"left"} or {"x":500,"y":300,"button":"right"}
List available presets
Execute a preset by name (e.g., /aria/preset/RIA3)
List allowed programs for the run() command. Returns names only (paths not exposed).
%APPDATA%\MusicBee\mbxhub.json to add/modify presets:
"ariaPresets": [
{"name": "RIA1", "script": "sndkeys(^%a)", "icon": "1"},
{"name": "DuckyDemo", "script": "sndkeys(CTRL ALT V)", "icon": "D"},
{"name": "Notify", "script": "toast(MBXHub,Hello World!)", "icon": "N"}
]
Script commands:sndkeys(keys) - SendKeys or DuckyScript: sndkeys(^a) or sndkeys(CTRL A). Prefix ! to skip refocus: sndkeys(!{ENTER})delay(ms) - Wait milliseconds (max 30000)click(x,y[,button]) - Mouse click: click(100,200) or click(100,200,right)volume(action) - Volume control: up, down, mute, or steps like +5/-3run(name[,extraArgs]) - Launch a pre-configured program: run(amp-on) or run(visualizer,--fullscreen). Programs must be defined in ariaAllowedPrograms in mbxhub.jsonwebhook(url[,method,body]) - HTTP request: webhook(http://example.com) or webhook(!http://...,POST,{}) (prefix ! for fire-and-forget)toast(msg) or toast(title,msg) - Show notificationrestart(target) - Restart: mb (MusicBee), system, or shutdownsndkeys(^a);delay(100);sndkeys(^c)
run() command only launches programs defined in ariaAllowedPrograms. Empty by default.
"ariaAllowedPrograms": [
{"name": "truedat", "path": "C:\\Program Files (x86)\\MusicBee\\Plugins\\truedat\\truedat.exe"},
{"name": "amp-on", "path": "C:\\Tools\\amp-control.exe", "args": "--power on", "hidden": true},
{"name": "visualizer", "path": "C:\\Program Files\\ProjectM\\projectm.exe"}
]
Each program has: name (used in scripts), path (executable), args (default arguments, optional), hidden (no console window, optional).
Extra arguments can be appended: run(visualizer,--fullscreen).
Publish MusicBee as a Windows RemoteApp, allowing the full desktop UI to be accessed from other machines via RDP. Requires Windows Pro, Enterprise, or Server — Home edition is not supported.
Check RemoteApp status. Always accessible.
Response:
{
"configured": false,
"supported": true,
"rdpEnabled": true,
"edition": "Professional",
"enabled": false,
"apiDisabled": false,
"message": "RemoteApp not configured. Run 'MBXHub.exe remoteapp setup' to configure."
}
Download a .rdp file for connecting to MusicBee as a RemoteApp. Blocked when remoteAppApiDisabled is true.
Query parameters:
hostname - Override the hostname in the .rdp file (defaults to request Host header)audioqualitymode=0, redirectprinters=1)Response: application/x-rdp file download (MusicBee.rdp)
403 when remoteAppApiDisabled is true.
Visibility: Dashboard footer link requires remoteAppEnabled and remoteAppApiDisabled: false. Footer links are configurable via dashboardFooterLinks in settings.
App program: On Windows Client (Pro/Enterprise), the .rdp file uses the full executable path from the registry.
On Windows Server, it uses the ||AppName alias for RDS published app lookup.
MBXHub.exe remoteapp setup --path "C:\MusicBee\MusicBee.exe" (requires elevation)MBXHub.exe remoteapp setup --detect (auto-detect MusicBee)MBXHub.exe remoteapp remove (remove configuration)MBXHub.exe remoteapp status (check current state)
Install-WindowsFeature RDS-Connection-Broker, RDS-Web-Access, RDS-RD-Server -IncludeManagementTools
Generic HTTP proxy for controlling LAN devices (speakers, receivers, home automation) from the browser.
Browsers enforce CORS on all cross-origin requests, and LAN devices don’t serve CORS headers —
direct fetch() from the dashboard to a device IP will silently fail. The proxy solves this by
forwarding requests server-side.
Forward an HTTP request to a LAN device. Only private IPs are allowed (RFC 1918).
Request body:
{
"method": "GET",
"url": "http://192.168.10.100/ipcontrol/v1/systems/current/sources",
"body": {}
}
method — HTTP method to use: GET, POST, or PUTurl — Full URL of the target device endpoint. Must be a private IP address.body — Optional JSON body to forward with POST/PUT requests.Response: The target device’s response is passed through verbatim (status code and body).
Errors:
POST /api/proxy → MBXHub → LAN deviceThe proxy can be disabled in Settings → API → Feature Toggles (apiDisableProxy).
When disabled, POST /api/proxy returns 404 and charm webapps that depend on it will not be able to reach LAN devices.
During an active party, the proxy is a Player-class action: guests without player permission are denied.
Serves audio files from the MusicBee library over HTTP with Range support.
Enables “Listen Here” mode in the player — the browser plays audio locally
via <audio> while MusicBee acts as the library manager.
Stream an audio or video file. The path must be URL-encoded and must be a file in the MusicBee library.
Path parameter: URL-encoded absolute file path (e.g. /stream/C%3A%5CMusic%5Csong.mp3).
Range support: Send Range: bytes=N-M header for partial content (required for seeking).
The server responds with 206 Partial Content and Content-Range header.
Supported formats: mp3, flac, m4a, mp4, ogg, oga, wav, opus, aac, wma, aiff, aif.
Actual browser playback depends on codec support — FLAC works in Firefox/Chrome/Edge,
WMA is not supported by any browser. The play page’s Listen Here mode probes
canPlayType up front and skips formats the browser reports it cannot decode,
naming the format on the output chip.
Security:
..) is blockedErrors:
Streaming can be disabled in Settings → API → Feature Toggles (disableStreaming).
When disabled, GET /stream/* returns 404 and the Listen Here button is hidden in the player.
While a party is active, streaming is additionally blocked by default (same 404) unless
partyAllowStreaming is enabled — see the PartyMode section.
Serves images and videos from configured root directories. Categories are subfolders under each root. No MusicBee library integration — purely a file-serving feature for slideshows and ambient display.
/media/* endpoint listed below is DJ-only. Guest and Anonymous callers get 403 PARTY_LOCKED with the message “Media browsing is locked during PartyMode (DJ-only).” The host’s Projector charm runs as DJ on localhost so the big-screen flow keeps working. Outside party mode, every caller is allowed (media is browse-only and not covered by the existing ApiReadOnly* gates).
Configure in mbxhub.json under media:
"media": {
"imageRoot": "C:\\Users\\...\\Pictures\\Wallpapers",
"videoRoot": "C:\\Users\\...\\Videos",
"intervalSeconds": 30,
"shuffle": true
}
Shorthand: set imageRoot to Pictures or videoRoot to
Videos (case-insensitive) to use the Windows profile's Pictures / Videos folder.
The actual location is looked up from the shell's known-folder registry, so redirected or
relocated profile folders resolve correctly.
Each subfolder of a root becomes a category. Files sitting directly in the root (not in
any subfolder) are still included — they appear under a pseudo-category named
_root. Applies to both images and videos.
Returns timer config: {intervalSeconds, shuffle}
List subfolder names under imageRoot. Files directly in root appear as _root.
List subfolder names under videoRoot. Files directly in root appear as _root.
List filenames in category. Returns {name, files:[], count}. Filenames only, no paths.
List video filenames in category.
Serve the next image (rotation state per category, sequential or shuffled). Returns image binary.
Serve the next video with HTTP Range support for seeking.
Serve a specific image by filename.
Serve a specific video by filename. Supports HTTP Range requests (206 Partial Content) for seeking.
Images: .jpg, .jpeg, .png, .bmp, .webp, .gif
Videos: .mp4, .webm, .mkv, .avi, .mov, .asf, .wmv (.mp4/.webm play inline; others open externally)
Minimum file size: 5 KB (skips thumbnail artifacts).
All paths validated under configured root — no directory traversal. Category names mapped to actual subfolders. Filenames validated against directory contents. Unconfigured roots return empty categories.
/pages/media — Full-bleed media viewer with auto-rotation,
crossfade transitions, video playback, chrome overlay with category/mode selectors,
keyboard (arrows, space, F) and touch/swipe navigation.
Control audio volume across three layers: MusicBee player volume (via /player/volume),
the Windows audio device, and network endpoints (e.g. Devialet Phantom speakers).
The Mixer charm provides a unified fader mixing surface for all three.
List all active Windows audio render devices (Core Audio ground truth)
// Response:
[
{ "name": "Speakers (HD Audio)", "id": "{0.0.0...}", "isDefault": true },
{ "name": "HDMI Output", "id": "{0.0.0...}", "isDefault": false }
]
Get Windows audio device volume and mute state
// Response:
{
"device": "Speakers (HD Audio)",
"volume": 75,
"muted": false
}
Set Windows audio device volume (0–100)
PUT /devices/audio/volume
Content-Type: application/json
{ "volume": 50 }
Set Windows audio device mute state
PUT /devices/audio/mute
Content-Type: application/json
{ "mute": true }
Manage network audio endpoints (speakers, receivers). Endpoints are saved in settings and controlled via REST. Currently supports Devialet Phantom speakers.
List all configured network endpoints
// Response:
[
{
"id": "devialet-1",
"name": "Living Room",
"type": "Devialet",
"ip": "192.168.1.50"
}
]
Add a new network endpoint
POST /devices/endpoints
Content-Type: application/json
{
"ip": "192.168.1.50",
"type": "devialet",
"name": "Living Room"
}
Scan the LAN for network speakers via mDNS/DNS-SD. Bodyless POST
(send Content-Length: 0). Blocks for ~4 seconds while the multicast
discovery completes. Concurrent calls are rate-limited internally and return an
empty list with a warning.
// Response:
[
{
"name": "Living Room Phantom",
"ip": "192.168.1.50",
"port": 80,
"hostName": "phantom-abc123.local",
"serviceType": "_devialet._tcp.local.",
"alreadyConfigured": false,
"existingId": null
}
]
Remove a saved endpoint
Get endpoint volume and mute state
// Response:
{
"volume": 40,
"muted": false
}
Set endpoint volume (0–100)
PUT /devices/endpoint/{id}/volume
Content-Type: application/json
{ "volume": 50 }
Set endpoint mute state
PUT /devices/endpoint/{id}/mute
Content-Type: application/json
{ "mute": true }
List available input sources on the endpoint
// Response:
[
{ "sourceId": "upnp", "name": "UPnP", "type": "upnp", "active": true },
{ "sourceId": "optical", "name": "Optical", "type": "optical", "active": false },
{ "sourceId": "analog", "name": "Analog", "type": "analog", "active": false }
]
Select an input source on the endpoint
PUT /devices/endpoint/{id}/source
Content-Type: application/json
{ "sourceId": "upnp" }
Configure which fader the dashboard volume controls target by default.
Get mixer settings
// Response:
{
"defaultFader": "player"
}
Set mixer settings. defaultFader controls which fader the dashboard volume slider and keyboard shortcuts target.
PUT /mixer/settings
Content-Type: application/json
// Values: "player", "device", "endpoint"
{ "defaultFader": "device" }
Get current volume from the active default fader (player, device, or endpoint). Returns fader type and volume (0-100). Endpoint mode resolves the first entry in endpoints[] (same “first endpoint is active” convention used by /pages/mixer.html); soft-falls-back to player if none configured or the speaker is unreachable, surfacing the actual fader hit in the response.
// Response (player fader):
{ "fader": "player", "volume": 75 }
// Response (device fader):
{ "fader": "device", "volume": 80, "muted": false, "device": "Speakers" }
// Response (endpoint fader):
{ "fader": "endpoint", "volume": 60, "muted": false, "endpoint": "Phantom" }
Set volume on the active default fader. Also accepts POST. In endpoint mode the response surfaces the actual fader hit — if no endpoint is configured or the speaker call fails, the server soft-falls-back to Player_SetVolume and returns fader: "player".
PUT /mixer/volume
Content-Type: application/json
{ "volume": 80 }
// Response (player or device fader):
{ "fader": "device", "volume": 80, "success": true }
// Response (endpoint fader, speaker reached):
{ "fader": "endpoint", "volume": 80, "success": true, "endpoint": "Phantom" }
// Response (endpoint fader, no endpoint configured -- fell back to player):
{ "fader": "player", "volume": 80, "success": true }
Charms are configurable action buttons on the dashboard. They can open webapps, fire HTTP requests to LAN devices, or call MBXHub endpoints. The charm bar appears as a dashboard section and can be reordered/hidden like any other panel.
Each charm is a .json file in the charms/ folder inside the MBXHub data directory.
MBXHub seeds built-in charms (mixer.json, browse.json) on first run.
Simple charm (single button):
{
"id": "my-charm",
"icon": "\uD83D\uDD0A",
"label": "My Charm",
"action": "webapp /pages/my-charm.html",
"display": "both",
"msg": "My Charm"
}
Expand charm (grouped buttons, e.g. Mixer):
{
"id": "mixer",
"label": "Mixer",
"expand": [
{ "icon": "\uD83C\uDFA8", "label": "Mixer", "action": "webapp /pages/mixer.html", "display": "both" },
{ "icon": "+", "label": "Volume Up", "action": "iframe-cmd volumeUp", "msg": "Vol +" },
{ "icon": "\u2013", "label": "Volume Down", "action": "iframe-cmd volumeDown", "msg": "Vol \u2013" },
{ "icon": "\uD83D\uDD07", "label": "Mute", "action": "iframe-cmd toggleMute", "msg": "Mute" }
]
}
id — Unique identifier (matches filename without extension).icon — Emoji or character displayed on the button.label — Tooltip and display name.action — What happens on click:
webapp /path — Opens an HTML page (standalone tab or inline iframe).iframe-cmd <command> — Sends a postMessage command to the charm iframe. The iframe webapp listens for { charmCmd: "command" } messages. If the iframe isn’t loaded yet, the dashboard auto-loads it from the sibling webapp action.http://... — Fires an HTTP request. Same-origin requests go direct; cross-origin LAN requests are routed through /api/proxy automatically.display — How webapp charms open:
standalone — Always opens in a new tab.inline — Always opens in an iframe below the charm bar.both — Click opens inline; Shift+click opens standalone.action-menu — Single trigger + popover of expand[] items (e.g. ARiA presets). Popover auto-orients above/below trigger; keyboard nav and Esc-to-close.msg — Status message shown after execution.context — Optional. library-only or stream-only to restrict when the charm appears.expand — Optional array of sub-actions. Each sub-action has icon, label, action, display, and msg. Renders as a grouped button row with a connection status indicator.Charm bar order, visibility, sizing, and per-charm overrides are stored in charmBar in mbxhub.json:
{
"charmBar": {
"order": ["mixer"],
"hidden": [],
"buttonSize": "M",
"sizeOverrides": { "mixer": "XL" },
"breakBefore": ["mixer"],
"displayOverrides": { "mixer": "inline" }
}
}
order — Charm IDs in display order. New charms not in the list are appended.hidden — Charm IDs to hide from the dashboard.buttonSize — Bar-level button size preset: S (36px), M (44px, default), L (52px), XL (64px), XXL (76px). S and M were bumped up in v0.5.2.1 (was 32/40); XL and XXL were added in v0.5.2.3 for comfortable touch on high-density displays (11.6″ FHD @ 150% DPI needs ~76px to hit a 0.6″ physical target).sizeOverrides — Per-charm size overrides (charm ID → S/M/L/XL/XXL). Individual charms can break from the bar-level size; the override wins via CSS specificity.breakBefore — Array of charm IDs that force a new row in the charm bar. Combined with sizeOverrides, one charm (e.g. the mixer) can sit on its own XL row while the rest stay compact.displayOverrides — Per-charm display mode (charm ID → inline or standalone). Overrides the charm manifest's default.The charm bar also appears as “Charm Bar” in the Dashboard Layout panel ordering, so it can be repositioned or collapsed like any other section.
Optional. The plugin (mb_MBXHub.dll) is fully functional on its own — REST, WebSocket, dashboard, AutoQ, charms, and discovery all work without the Shell. The Shell adds Windows-side conveniences: SMTC integration, a system-tray UI for switching SMTC targets and opening dashboards across the fleet, Windows app identity (AUMID, Start Menu shortcut), and a firewall helper that adds / removes the Windows Firewall rule and URL ACL on --install / --uninstall.
The Shell binds the SMTC bridge on restPort + 1 (default 8081). Local use (Shell and plugin on the same box) needs no firewall config — loopback traffic passes through. Remote use (Shell on one machine bridging to MBXHub on another) needs the SMTC port open on the machine hosting the Shell, so the remote dashboard can reach /meta/smtc/*. MBXHub.exe --install opens it for you.
Key commands for the MBXHub Shell (MBXHub.exe):
| Command | Description |
|---|---|
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, report tool discovery, print Send To setup tip |
MBXHub.exe --uninstall | Remove AUMID registration and shortcut |
Direct access to all 137 MusicBee API methods. Use this for operations not exposed via REST endpoints or for scripting/automation.
Invoke any MusicBee API method by name
POST /rpc/Library_GetFileTag
Content-Type: application/json
{
"fileUrl": "C:\\Music\\song.mp3",
"field": "TrackTitle"
}
// Response:
{
"success": true,
"data": {
"method": "Library_GetFileTag",
"result": "Song Title"
}
}
| Method | Parameters | Returns |
|---|---|---|
| Player_PlayPause | - | boolean |
| Player_Stop | - | boolean |
| Player_StopAfterCurrent | - | boolean |
| Player_PlayNextTrack | - | boolean |
| Player_PlayPreviousTrack | - | boolean |
| Player_PlayNextAlbum | - | boolean |
| Player_PlayPreviousAlbum | - | boolean |
| Player_StartAutoDj | - | boolean |
| Player_EndAutoDj | - | boolean |
| Player_GetPosition | - | int (ms) |
| Player_SetPosition | position: int | boolean |
| Player_GetVolume | - | float (0-1) |
| Player_SetVolume | volume: float | boolean |
| Player_GetMute | - | boolean |
| Player_SetMute | mute: boolean | boolean |
| Player_GetShuffle | - | boolean |
| Player_SetShuffle | shuffle: boolean | boolean |
| Player_GetRepeat | - | RepeatMode |
| Player_SetRepeat | mode: RepeatMode | boolean |
| Player_GetPlayState | - | PlayState |
| Player_GetEqualiserEnabled | - | boolean |
| Player_SetEqualiserEnabled | enabled: boolean | boolean |
| Player_GetDspEnabled | - | boolean |
| Player_SetDspEnabled | enabled: boolean | boolean |
| Player_GetCrossfade | - | boolean |
| Player_SetCrossfade | enabled: boolean | boolean |
| Player_GetReplayGainMode | - | ReplayGainMode |
| Player_SetReplayGainMode | mode: ReplayGainMode | boolean |
| Player_GetScrobbleEnabled | - | boolean |
| Player_SetScrobbleEnabled | enabled: boolean | boolean |
| Player_QueueRandomTracks | count: int | int |
| Player_GetOutputDevices | - | {devices, activeDevice} |
| Player_SetOutputDevice | deviceName: string | boolean |
| Method | Parameters | Returns |
|---|---|---|
| NowPlaying_GetFileUrl | - | string |
| NowPlaying_GetDuration | - | int (ms) |
| NowPlaying_GetFileTag | field: MetaDataType | string |
| NowPlaying_GetFileTags | fields: MetaDataType[] | string[] |
| NowPlaying_GetFileProperty | type: FilePropertyType | string |
| NowPlaying_GetArtwork | - | string (base64/path) |
| NowPlaying_GetArtworkUrl | - | string |
| NowPlaying_GetLyrics | - | string |
| NowPlaying_GetDownloadedLyrics | - | string |
| NowPlaying_GetArtistPicture | fadingPercent: int | string |
| NowPlaying_GetArtistPictureThumb | - | string |
| NowPlaying_GetArtistPictureUrls | localOnly: boolean | string[] |
| NowPlaying_IsSoundtrack | - | boolean |
| NowPlaying_GetSpectrumData | - | float[] |
| NowPlaying_GetSoundGraph | - | float[] |
| Method | Parameters | Returns |
|---|---|---|
| NowPlayingList_GetCurrentIndex | - | int |
| NowPlayingList_GetNextIndex | offset: int | int |
| NowPlayingList_IsAnyPriorTracks | - | boolean |
| NowPlayingList_IsAnyFollowingTracks | - | boolean |
| NowPlayingList_GetListFileUrl | index: int | string |
| NowPlayingList_GetFileTag | index: int, field: MetaDataType | string |
| NowPlayingList_GetFileTags | index: int, fields: MetaDataType[] | string[] |
| NowPlayingList_GetFileProperty | index: int, type: FilePropertyType | string |
| NowPlayingList_Clear | - | boolean |
| NowPlayingList_PlayNow | fileUrl: string | boolean |
| NowPlayingList_QueueNext | fileUrl: string | boolean |
| NowPlayingList_QueueLast | fileUrl: string | boolean |
| NowPlayingList_QueueFilesNext | fileUrls: string[] | boolean |
| NowPlayingList_QueueFilesLast | fileUrls: string[] | boolean |
| NowPlayingList_RemoveAt | index: int | boolean |
| NowPlayingList_MoveFiles | fromIndices: int[], toIndex: int | boolean |
| NowPlayingList_PlayLibraryShuffled | - | boolean |
| NowPlayingList_QueryFilesEx | query: string | string[] |
| Method | Parameters | Returns |
|---|---|---|
| Library_GetFileTag | fileUrl: string, field: MetaDataType | string |
| Library_GetFileTags | fileUrl: string, fields: MetaDataType[] | string[] |
| Library_GetFileProperty | fileUrl: string, type: FilePropertyType | string |
| Library_SetFileTag | fileUrl: string, field: MetaDataType, value: string | boolean |
| Library_CommitTagsToFile | fileUrl: string | boolean |
| Library_GetLyrics | fileUrl: string, type: LyricsType | string |
| Library_GetArtwork | fileUrl: string, index: int | string |
| Library_GetArtworkUrl | fileUrl: string, index: int | string |
| Library_GetArtistPicture | artistName: string, fadingPercent: int | string |
| Library_GetArtistPictureThumb | artistName: string | string |
| Library_GetArtistPictureUrls | artistName: string, localOnly: boolean | string[] |
| Library_QueryFilesEx | query: string | string[] |
| Library_QuerySimilarArtists | artistName: string, minimumSimilarity: double | string |
| Library_AddFileToLibrary | fileUrl: string, category: LibraryCategory | string |
| Method | Parameters | Returns |
|---|---|---|
| Playlist_QueryPlaylists | - | boolean |
| Playlist_QueryGetNextPlaylist | - | string |
| Playlist_GetName | playlistUrl: string | string |
| Playlist_GetType | playlistUrl: string | PlaylistFormat |
| Playlist_IsInList | playlistUrl: string, filename: string | boolean |
| Playlist_QueryFilesEx | playlistUrl: string | string[] |
| Playlist_CreatePlaylist | folderName: string, playlistName: string, filenames: string[] | string |
| Playlist_DeletePlaylist | playlistUrl: string | boolean |
| Playlist_SetFiles | playlistUrl: string, filenames: string[] | boolean |
| Playlist_AppendFiles | playlistUrl: string, filenames: string[] | boolean |
| Playlist_RemoveAt | playlistUrl: string, index: int | boolean |
| Playlist_MoveFiles | playlistUrl: string, fromIndices: int[], toIndex: int | boolean |
| Playlist_PlayNow | playlistUrl: string | boolean |
| Method | Parameters | Returns |
|---|---|---|
| Podcasts_QuerySubscriptions | query: string | string[] |
| Podcasts_GetSubscription | id: string | string[] |
| Podcasts_GetSubscriptionArtwork | id: string, index: int | string (base64) |
| Podcasts_GetSubscriptionEpisodes | id: string | string[] |
| Podcasts_GetSubscriptionEpisode | id: string, index: int | string[] |
| Method | Parameters | Returns |
|---|---|---|
| Setting_GetPersistentStoragePath | - | string |
| Setting_GetSkin | - | string |
| Setting_GetSkinElementColour | element: SkinElement, state: ElementState, component: ElementComponent | int |
| Setting_IsWindowBordersSkinned | - | boolean |
| Setting_GetFieldName | field: MetaDataType | string |
| Setting_GetDataType | field: MetaDataType | string |
| Setting_GetLastFmUserId | - | string |
| Setting_GetWebProxy | - | string |
| Setting_GetValue | settingId: SettingId | object |
| Method | Parameters | Returns |
|---|---|---|
| MB_GetWindowHandle | - | long |
| MB_RefreshPanels | - | true |
| MB_GetLocalisation | id: string, defaultText: string | string |
| MB_ShowNowPlayingAssistant | - | boolean |
| MB_InvokeCommand | command: Command, parameter: object | boolean |
| MB_SetWindowSize | width: int, height: int | boolean |
| MB_GetVisualiserInformation | - | {visualiserNames, defaultState, currentState} |
| MB_ShowVisualiser | visualiserName: string, state: WindowState | boolean |
"rpcEnabled": falsePlayer_PlayPause, Library_SetFileTag) require the same permissions as their REST equivalents.
Real-time event streaming. Connect once, receive updates automatically - no polling.
ws://localhost:8080/ws| Step | Description |
|---|---|
| 1. Connect | Open WebSocket to ws://localhost:8080/ws |
| 2. Receive | Immediately starts receiving ALL events (default behavior) |
| 3. Subscribe (optional) | Send subscribe message to filter to specific events only |
| 4. Disconnect | Close the WebSocket connection when done |
Note: New clients receive ALL events by default. Once you send a subscribe message, you only receive those specific events. Use unsubscribe to stop receiving events without disconnecting.
| Event | Description | Frequency |
|---|---|---|
| TrackChanged | New track started playing | On track change |
| PlayStateChanged | Play/pause/stop state changed | On state change |
| VolumeChanged | Volume level or mute state changed | On volume change |
| PositionChanged | Playback position update (milliseconds) | ~1 per second while playing |
| QueueChanged | Now playing list modified (add/remove/clear) | On queue change |
| ShuffleChanged | Shuffle mode toggled on/off | On shuffle change |
| RepeatChanged | Repeat mode changed (none/all/one) | On repeat change |
| TempoChanged | Playback tempo changed (MB 3.5+; relays the file, no tempo value in the MB API) | On tempo change |
| MetadataChanged | Rating or love tag changed on current track | On tag/rating change |
| Reaction | User reacted to now playing track (emoji, nickname, track info) | On reaction submit |
| TasteChanged | AutoQ taste vector updated | On taste update |
| ThemeChanged | Theme configuration updated (active mode HSL values) | On theme change via PUT /system/theme or dashboard toggle |
| SearchMatched | v0.5.3.0. Saved-search match set changed. Payload: {id, name, added:[urls], removed:[urls], total, evaluatedAt} | Periodic SavedSearchScheduler tick when the diff is non-empty |
| MoodCacheReady | v0.5.3.2. Server-side mood cache finished cold-start load — dashboard mood pill is now fillable. Payload: {entries: N} (count of moods now in cache). Dashboard's WS handler calls softUpdate on receipt; replaces the previous 1500ms server-side Thread.Sleep that bridged the cold-cache window. | One-shot, fired once per plugin start after MoodCache phase 3 completes |
| PartyStateChanged | Party mode started or ended. Payload: {isActive}. Lets already-open pages redirect to the party surface the instant a party starts, instead of polling /partymode/status. Fresh page loads are handled server-side by a 302 redirect. | On StartParty / StopParty (REST endpoint or WinForms dialog) |
// Subscribe to specific events (filters to only these events)
{"subscribe": ["TrackChanged", "PlayStateChanged"]}
// Unsubscribe from events (stop receiving them)
{"unsubscribe": ["PositionChanged"]}
// Subscribe to all events (equivalent to no subscriptions)
{"subscribe": ["TrackChanged", "PlayStateChanged", "VolumeChanged",
"PositionChanged", "QueueChanged", "ShuffleChanged",
"RepeatChanged", "MetadataChanged", "Reaction",
"TasteChanged", "ThemeChanged", "SearchMatched",
"MoodCacheReady", "PartyStateChanged"]}
// TrackChanged
// cueTrack + cueStartMs are present only when the current track is a
// CUE-backed virtual track (one physical file, multiple logical tracks).
// Clients that stream audio locally use cueStartMs to seek <audio>.currentTime.
{
"event": "TrackChanged",
"timestamp": "2024-01-03T12:00:00.000Z",
"data": {
"fileUrl": "C:\\Music\\song.mp3",
"title": "Track Title",
"artist": "Artist Name",
"album": "Album Name",
"duration": 245000,
"artworkUrl": "/nowplaying/artwork",
"cueTrack": 3, // optional
"cueStartMs": 184000 // optional
}
}
// PlayStateChanged
{
"event": "PlayStateChanged",
"timestamp": "2024-01-03T12:00:00.000Z",
"data": {
"state": "playing" // "playing", "paused", "stopped"
}
}
// VolumeChanged
{
"event": "VolumeChanged",
"timestamp": "2024-01-03T12:00:00.000Z",
"data": {
"volume": 75, // 0 to 100
"muted": false
}
}
// PositionChanged
{
"event": "PositionChanged",
"timestamp": "2024-01-03T12:00:00.000Z",
"data": {
"position": 45000, // Current position in milliseconds
"duration": 245000 // Total duration in milliseconds
}
}
// QueueChanged
{
"event": "QueueChanged",
"timestamp": "2024-01-03T12:00:00.000Z",
"data": {
"action": "add", // "add", "remove", "clear", "move"
"index": 5,
"totalTracks": 42
}
}
// ShuffleChanged
{
"event": "ShuffleChanged",
"timestamp": "2024-01-03T12:00:00.000Z",
"data": {
"enabled": true
}
}
// RepeatChanged
{
"event": "RepeatChanged",
"timestamp": "2024-01-03T12:00:00.000Z",
"data": {
"mode": "all" // "none", "all", "one"
}
}
// MetadataChanged
{
"event": "MetadataChanged",
"timestamp": "2024-01-03T12:00:00.000Z",
"data": {
"fileUrl": "C:\\Music\\song.mp3",
"rating": 3, // -1 (unrated) to 5
"love": "L" // "L" (loved), "B" (banned), or "" (neither)
}
}
// Reaction
{
"event": "Reaction",
"timestamp": "2024-01-03T12:00:00.000Z",
"data": {
"emoji": "fire",
"type": "fire", // fire, heart, like, dislike, ban
"nickname": "Guest",
"trackTitle": "Song Name",
"trackArtist": "Artist"
}
}
// TasteChanged (debounced, fires after reactions/influences/mood changes)
{
"event": "TasteChanged",
"timestamp": "2024-01-03T12:00:05.000Z",
"data": {
"topGenres": [{ "name": "Rock", "weight": 1.0 }],
"topArtists": [{ "name": "Foo Fighters", "weight": 0.85 }],
"bpmRange": [90, 160],
"mood": "Energetic",
"moodConfidence": 0.88,
"influenceCount": 3,
"reactionCount": 12
}
}
// ThemeChanged (fires on PUT /system/theme or dashboard mode toggle)
{
"event": "ThemeChanged",
"timestamp": "2024-01-03T12:00:06.000Z",
"data": {
"activeMode": 1,
"accentHue": 197, "accentSaturation": 80, "accentLightness": 55,
"bgHue": 203, "bgSaturation": 30, "bgLightness": 94,
"surfaceHue": 203, "surfaceSaturation": 30, "surfaceLightness": 96,
"textHue": 203, "textSaturation": 50, "textLightness": 13,
"intensity": 100
}
}
// Connect to WebSocket
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = function() {
console.log('Connected to MBXHub');
// Optional: Subscribe to specific events only
// Without this, you receive ALL events
ws.send(JSON.stringify({
subscribe: ['TrackChanged', 'PlayStateChanged', 'PositionChanged']
}));
};
ws.onmessage = function(event) {
const msg = JSON.parse(event.data);
switch (msg.event) {
case 'TrackChanged':
console.log('Now playing:', msg.data.title, '-', msg.data.artist);
break;
case 'PlayStateChanged':
console.log('State:', msg.data.state);
break;
case 'PositionChanged':
const pct = (msg.data.position / msg.data.duration * 100).toFixed(1);
console.log('Position:', pct + '%');
break;
}
};
ws.onclose = function() {
console.log('Disconnected from MBXHub');
};
ws.onerror = function(err) {
console.error('WebSocket error:', err);
};
// Later: Unsubscribe from position updates (too frequent)
ws.send(JSON.stringify({ unsubscribe: ['PositionChanged'] }));
// Clean disconnect
ws.close();
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description"
}
}
| Code | HTTP | Description |
|---|---|---|
| NOT_FOUND | 404 | Endpoint or resource not found |
| INVALID_REQUEST | 400 | Invalid parameters or malformed request |
| ARIA_DISABLED | 403 | ARiA input simulation is disabled |
| FORBIDDEN | 403 | Operation not allowed (e.g., RPC disabled) |
| AUTH_REQUIRED | 401 | The HTTP Basic password gate is enabled and the request lacked valid credentials. Response carries a WWW-Authenticate: Basic header. See Security → HTTP Basic Password Gate. |
| METHOD_NOT_ALLOWED | 405 | Wrong HTTP method |
| SERVICE_UNAVAILABLE | 503 | Required service not available (e.g., TrueShuffle/AutoQ) |
| INTERNAL_ERROR | 500 | Server error. Response body contains the full exception dump (type, message, stack trace, inner-exception chain) when debugMode is on and logLevel is Debug or lower; otherwise body is the generic fallback string. The dump always lands in mbxhub.log via _log.Warn(ex, ...) regardless of the gate. See Logging section below for how to enable. |
Common enum values used in API parameters and responses.
| Value | Code | Description |
|---|---|---|
| undefined | 0 | Unknown state |
| loading | 1 | Track is loading |
| playing | 3 | Playing |
| paused | 6 | Paused |
| stopped | 7 | Stopped |
| Value | Code | Description |
|---|---|---|
| none | 0 | No repeat |
| all | 1 | Repeat all tracks |
| one | 2 | Repeat current track |
| Value | Code | Description |
|---|---|---|
| off | 0 | Disabled |
| track | 1 | Track-based gain |
| album | 2 | Album-based gain |
| smart | 3 | Automatic selection |
| Field | Code | Description |
|---|---|---|
| TrackTitle | 65 | Track title |
| Album | 30 | Album name |
| AlbumArtist | 31 | Album artist |
| Artist | 32 | Track artist |
| Composer | 43 | Composer |
| Genre | 59 | Genre |
| Rating | 75 | Star rating (0-5) |
| RatingLove | 76 | Love rating |
| TrackNo | 86 | Track number |
| DiscNo | 52 | Disc number |
| Year | 88 | Year |
| Lyrics | 114 | Lyrics text |
| Comment | 44 | Comment |
| Publisher | 73 | Publisher/Label |
| Conductor | 45 | Conductor |
| Property | Code | Description |
|---|---|---|
| Url | 2 | File path/URL |
| Kind | 4 | File type (Music, Video, etc.) |
| Format | 5 | Audio format (MP3, FLAC, etc.) |
| Size | 7 | File size in bytes |
| Channels | 8 | Audio channels |
| SampleRate | 9 | Sample rate (Hz) |
| Bitrate | 10 | Bitrate (kbps) |
| Duration | 16 | Duration (ms) |
| PlayCount | 14 | Play count |
| SkipCount | 15 | Skip count |
| LastPlayed | 13 | Last played date |
| DateAdded | 12 | Date added to library |
| DateModified | 11 | File modification date |
| Format | Code | Description |
|---|---|---|
| Unknown | 0 | Unknown format |
| M3u | 1 | M3U playlist |
| Xspf | 2 | XSPF (XML Shareable Playlist) |
| Asx | 3 | ASX (Windows Media) |
| Wpl | 4 | WPL (Windows Media) |
| Pls | 5 | PLS playlist |
| Auto | 7 | Auto-detect format |
| Type | Code | Description |
|---|---|---|
| NotSpecified | 0 | Any lyrics type |
| Synchronised | 1 | Time-synced lyrics (LRC) |
| UnSynchronised | 2 | Plain text lyrics |
| Category | Code | Description |
|---|---|---|
| Music | 0 | Music files |
| Audiobook | 1 | Audiobooks |
| Video | 2 | Video files |
| Inbox | 4 | Inbox (new files) |
MBXHub includes comprehensive logging for debugging and monitoring. Logs are written using NLog to a log file in the MBXHub folder.
Logging is controlled by two settings in mbxhub.json:
| Setting | Type | Description |
|---|---|---|
debugMode | boolean | Master switch - enables/disables all logging. Also gates exception dumps in 500 response bodies (see below). |
logLevel | string | Minimum log level when debug mode is on |
500 response body gate: when debugMode is true AND logLevel is Debug or Trace, INTERNAL_ERROR responses include the full exception dump in the body (type, message, stack trace, inner-exception chain) — useful when debugging a self-hosted LAN deployment where the operator is also the consumer. Otherwise the body is the generic fallback string and the dump only lands in mbxhub.log. Default is fail-secure (debugMode: false) so production / shared exposure does not leak internals.
| Level | What Gets Logged |
|---|---|
| Trace | Everything including request/response bodies, WebSocket message content. Very verbose. |
| Debug | Route matching, handler selection, subscription changes, internal decisions. |
| Info | Startup/shutdown, HTTP requests (method/path/status/timing), connections, track changes. |
| Warning | Recoverable errors, timeouts, retries, unexpected but handled situations. |
| Error | Failures, exceptions, service unavailable. Always logged even with debug mode off. |
Log files are stored in the MBXHub subfolder of MusicBee's persistent storage:
%AppData%\MusicBee\MBXHub\mbxhub.log
mbxhub.1.log, mbxhub.2.log, etc.To enable debug logging:
// In mbxhub.json:
{
"debugMode": true,
"logLevel": "Trace" // or "Debug", "Info", "Warning", "Error"
}
HTTP requests are logged at Info level with timing:
2025-01-25 14:32:15 [INFO ] [REST] HTTP GET /player/status -> 200 (12ms)
2025-01-25 14:32:16 [INFO ] [REST] HTTP POST /player/playpause -> 200 (8ms)
2025-01-25 14:32:17 [INFO ] [Plugin] Track changed: Artist Name - Track Title
2025-01-25 14:32:17 [INFO ] [WebSocket] WS client abc12345 connected from 192.168.1.50:54321
MBXHub operates on a trusted local network model with multiple security layers.
PartyMode uses PIN-based authentication with three roles:
| Role | Access | Authentication |
|---|---|---|
DJ | Full control: player, queue, start/stop party | DJ PIN via X-Party-PIN header |
Guest | Browse library, request songs, vote | Guest PIN via X-Party-PIN header |
Anonymous | Read-only: now playing, artwork, status | No PIN required |
PartyMode endpoints authenticate via the X-Party-PIN header:
# Guest request example
curl -X POST http://localhost:8080/partymode/request \
-H "X-Party-PIN: 1234" \
-H "Content-Type: application/json" \
-d '{"url":"C:\\Music\\Track.mp3","nickname":"Haro"}'
# Validate PIN and get role
GET /partymode/validate?pin=1234&nickname=Haro
Invalid or missing PIN returns 401 Unauthorized.
An optional always-on password gate over the whole API surface — a safety net for trusted LANs. Turn it on in the plugin under Settings → Remote Connection Settings (Configure Access…) → Remote Access by ticking Require password (HTTP Basic) and setting a password. It is off by default and has no web/API configuration.
When enabled, remote requests must carry an HTTP Basic credential. The username is ignored; only the password is checked:
# Any username, the configured password
curl http://localhost:8080/library/files \
-H "Authorization: Basic $(printf 'x:yourpassword' | base64)"
Missing or wrong credentials return 401 Unauthorized with a WWW-Authenticate: Basic realm="MBXHub" header, so browsers show a native login prompt. Exempt from the gate: localhost (the host machine), the /ws event stream, and the live-PartyMode surface (party guests authenticate with their PIN, not this password).
Limits — this is a speed bump, not transport security. Over plain HTTP the password is sent base64-encoded (effectively cleartext) on every request; only HTTPS fixes that. The stored password is an unsalted SHA-256 hash. Fine for a single shared secret on a trusted network, not a hostile one.
The password is stored only as a one-way hash — never in plain text, and it cannot be recovered. If you forget it, set a new one in the same Remote Access settings where you enabled the gate.
MBXHub supports three protection levels for different deployment scenarios:
| Level | Description | Use Case |
|---|---|---|
| Default | Full API access, no restrictions | Personal use, trusted networks |
| Kiosk | All requests redirect to defaultPage | Party displays, public screens |
| Restricted | Read-only mode with granular controls | Shared access, limited control |
Configure via kioskMode and apiReadOnlyMode in settings.
PartyMode includes built-in limits and optional per-IP rate limiting:
Built-in limits (always active):
Per-IP rate limiting (configurable via Settings → Party Mode...):
Returns 429 Too Many Requests when limits exceeded.
| Origin | Access |
|---|---|
localhost, 127.0.0.1 | Always allowed |
192.168.x.x | Allowed when allowRemoteConnections is enabled |
10.x.x.x | Allowed when allowRemoteConnections is enabled |
172.16.x.x - 172.31.x.x | Allowed when allowRemoteConnections is enabled |
| External origins | Blocked (prevents cross-site attacks) |
MBXHub can restrict write operations via settings with granular per-operation controls:
Restriction Hierarchy:
Settings cascade: master OR category OR operation = blocked
Granular Operations:
| Category | Operation | Endpoints Affected |
|---|---|---|
| Library | Tag edits | PUT /library/file/*, POST /library/commit |
| Queue | Add tracks | POST /queue/add, /queue/playnow, /queue/play |
| Remove tracks | DELETE /queue/* | |
| Reorder tracks | POST /queue/move | |
| Player | Playback | POST /player/play, /pause, /stop, /next, /previous |
| Volume | POST /player/volume, /mute | |
| Seek | POST /player/position | |
| Playlists | Create | POST /playlists |
| Delete | DELETE /playlists/* | |
| Modify | PUT /playlists/*, POST /playlists/*/files |
Default Settings:
The philosophy: player and queue are open, destructive operations are locked. Playlists, tag edits, and file deletion default to read-only.
| Setting | Default | Effect |
|---|---|---|
apiReadOnlyMode | false | Master switch — off |
apiReadOnlyPlayer | false | Player controls allowed |
apiReadOnlyQueue | false | Queue modifications allowed |
apiReadOnlyLibrary | false | Library allowed (but see granular) |
apiReadOnlyPlaylists | true | Playlists blocked by default |
apiReadOnlyLibraryTags | true | Tag edits blocked by default |
apiReadOnlyLibraryDelete | true | File deletion blocked by default |
apiReadOnlyPlayerPlayback | false | Playback allowed |
apiReadOnlyPlayerVolume | false | Volume allowed |
apiReadOnlyPlayerSeek | false | Seek allowed |
apiReadOnlyQueueAdd | false | Queue add allowed |
apiReadOnlyQueueRemove | false | Queue remove allowed |
apiReadOnlyQueueReorder | false | Queue reorder allowed |
apiReadOnlyPlaylistsCreate | false | (moot — parent is true) |
apiReadOnlyPlaylistsDelete | false | (moot — parent is true) |
apiReadOnlyPlaylistsModify | false | (moot — parent is true) |
Settings cascade: master OR category OR operation = blocked. The granular playlist settings default to false but are moot because their parent apiReadOnlyPlaylists is true.
Action Categories (v0.5.2.6+):
| Category | Endpoints | Notes |
|---|---|---|
| MediaHandler party-mode | /media/* | When a party is active, all /media/* endpoints are DJ-only. Guest/Anonymous ⇒ 403 PARTY_LOCKED. The host’s Projector charm runs as DJ on localhost. Outside party mode, every caller is allowed. |
Always Permitted (exempt from restrictions):
Blocked requests return 403 Forbidden:
{"success":false,"error":{"code":"READ_ONLY","message":"API is in read-only mode"}}
{"success":false,"error":{"code":"PARTY_LOCKED","message":"Media browsing is locked during PartyMode (DJ-only)."}}
allowRemoteConnections disabled unless neededrpcEnabled: false) if not neededariaEnabled: false) unless specifically needed for PC wake scenariosrun() command only launches programs defined in ariaAllowedPrograms. Do not add shell interpreters (cmd.exe, powershell.exe) to the allowlist
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
// Example: GET /library/file/C%3A%5CMusic%5CArtist%5CTrack.mp3
Most list endpoints support offset and limit query parameters:
GET /library/files?offset=100&limit=50Library endpoints support ?sort= parameter for server-side sorting:
alpha (default) - Alphabetical by title+artistartist - By artist name, then titlealbum - By album name, then track numbertitle - By title onlydate - By date added (newest first)track - By disc number, then track number (natural album order)name - By display nameyear-asc - By year ascending (chronological, oldest first)Example: GET /library/files?artist=Pink+Floyd&sort=album
v0.5.3.4 behavior notes. String comparisons use InvariantCultureIgnoreCase over diacritic-folded keys, so non-Latin album titles (Cyrillic / Greek / CJK / Hebrew / Arabic) sort under their own scripts instead of dropping to the tail of an A→Z list (ASCII libraries unchanged). Custom-* sorts configured as Numeric send rows with unparseable values (Year=1999/2000 on compilations, whitespace-padded fields) to the END of the list alongside other non-Numerics, matching the Auto kind's segregation — previously they landed at the TOP via a long.MinValue sentinel.
Shuffle, banlist, and influence endpoints require TrueShuffle or AutoQ to be enabled. Check availability:
GET /shuffle/status
// Returns 503 SERVICE_UNAVAILABLE if TrueShuffle/AutoQ not enabled
For real-time updates (track changes, state changes), use WebSocket instead of polling:
By default, MBXHub only accepts localhost connections. For network access:
allowRemoteConnections in settingshttp://192.168.1.100:8080)MBXHub automatically detects CUE-backed audio files and resolves per-track metadata across all surfaces:
/player/status) - Overlays title, artist, trackNo, album from CUE sheet/nowplaying/position) - Includes cueTrack and cueTitle/nowplaying) - Includes cueTrack and cueStartMs (browser-mode clients seek the container and report play stats against the right sub-track)/nowplaying/tag) - Returns CUE track data for TrackTitle, Artist, Album, TrackNotrack events include cueTrack field when CUE activeEncoding detection: BOM check → UTF-8 validation → Windows-1252 fallback. Built-in regex parser; no external CUE library required.
| Use REST when... | Use RPC when... |
|---|---|
| Building a client app | Writing automation scripts |
| Need clean, discoverable URLs | Need direct MusicBee API access |
| Want resource-oriented design | Familiar with MusicBee plugin API |
| Working with standard HTTP clients | Need parameter flexibility |