PREVIEWInstall MBXHub to explore the full interactive docs

MBXHub

The bridge to everything else

v0.5.2.4
Base URL: http://localhost:8080
WebSocket: ws://localhost:8080/ws
175+ endpoints covering all MusicBee API methods

API Categories

Quick Start

It'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

Dashboard

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).

GET /dashboard

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=.

Player Controls (POST)
POST/dashboard/play POST/dashboard/pause POST/dashboard/stop POST/dashboard/next POST/dashboard/prev POST/dashboard/previous
Volume (POST)
POST/dashboard/volup POST/dashboard/voldown POST/dashboard/volup1 POST/dashboard/voldown1 POST/dashboard/mute

volup/voldown - Adjust volume by 5%.
volup1/voldown1 - Adjust volume by 1% (fine control).

Shuffle/AutoDJ/Repeat (POST)
POST/dashboard/shuffle POST/dashboard/shuffle-off POST/dashboard/autodj POST/dashboard/repeat

shuffle=enable, shuffle-off=disable both, autodj=start AutoDJ, repeat=cycle mode

Rating & Metadata (POST)
POST/dashboard/love POST/dashboard/rate/{0-5} POST/dashboard/ban POST/dashboard/setban

love - Toggle love tag on current track.
rate/0 - Toggle bomb (don't play). Click on → click off.
rate/1-5 - Toggle star rating. 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.

AutoQ & Mood (POST)
POST/dashboard/mood POST/dashboard/react/{type} POST/dashboard/refresh-queue POST/dashboard/influence/{target}/{direction}

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.

Playlist (POST)
POST/dashboard/playlist

Play or queue a playlist (form: playlistUrl=...&action=now|next|last).

Theme
GET/dashboard/theme?mode={1|2}
Layout & Display Settings

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"):

StyleDescription
fullStandard layout with full-size artwork and metadata
horizontalSide-by-side artwork and metadata
noartNo artwork, text-only display
splitTwo-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
immersiveFull-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)

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%, 42%, 27%). The compact style was removed in v0.5.2.3 — existing configs gracefully fall back to full.

Static Pages

MBXHub can serve custom HTML pages from a configurable directory. This enables building custom web UIs that use the REST API.

How it works: Place HTML, CSS, JS, and image files in the pages directory. MBXHub serves them at /pages/ with correct Content-Type headers. Default pages are extracted on first run and can be customized without losing changes on updates.
Configuration

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/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).

Endpoints
GET/pages/

Serves index.html from the pages directory

GET/pages/{filename}

Serves any file from the pages directory (HTML, CSS, JS, images, fonts)

Default Pages
  • /pages/index.html - Landing page listing available views
  • /pages/player.html - Full-featured desktop player with:
    • Now Playing (artwork, title, artist, genre, lyrics)
    • Player controls (play/pause, prev/next, shuffle, repeat, volume, seek)
    • Browse panel (Artists, Albums, Genres, Playlists, Podcasts, Radio, Moods — empty categories auto-hidden)
    • Queue panel (Up Next with Now/Next/Last actions)
    • Vibe score badge, reaction buttons, mood channel selector
    • Influence thumbs, ARiA presets, Love/Ban buttons
    • Live WebSocket updates (including Reaction events)
    • Mobile tab bar (Now Playing / Browse / Queue), tablet 2-column, desktop 3-column with resizable panels
  • /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)
Customization

To customize pages:

  1. Navigate to the pages directory (shown in MBXHub settings)
  2. Edit player.html or create new HTML files
  3. Refresh the browser - changes appear immediately
  4. To reset to defaults, delete the pages folder and restart MusicBee
Build with AI

MBXHub serves /llms.txt - an AI-friendly API reference. Use it with Claude or any AI to generate custom pages:

  1. Tell Claude: "Read https://mbxhub.com/llms.txt and build me a Party-On-Mode page - big artwork, guest queue requests, vibe controls"
  2. Claude fetches the API cheat sheet (public URL works from any AI)
  3. Claude generates code using relative URLs (/nowplaying, /player/play) that work on any MBXHub instance
  4. Save to %APPDATA%\MusicBee\MBXHub\pages\
  5. Open http://localhost:8080/pages/partyon.html

The generated code uses relative URLs, so it works on your local MBXHub without modification.

Network Discovery

MBXHub advertises itself on the local network via three protocols so clients can find it automatically.

SSDP/UPnP (UDP 1900): Periodic NOTIFY messages to multicast 239.255.255.250:1900. UPnP control points and DLNA clients discover MBXHub and retrieve its device description.
WS-Discovery (UDP 3702): Hello/Bye/ProbeMatch messages to multicast 239.255.255.250:3702. Makes MBXHub appear automatically in the Windows Explorer Network folder with a clickable "Device webpage" link to the dashboard.
mDNS/DNS-SD (UDP 5353): Bonjour/zero-conf service advertisement via Windows native 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.
GET /device.xml

UPnP device description XML. Contains device info, service URLs, and presentation URL.

POST /wsd

WS-Discovery metadata exchange endpoint. Windows sends a SOAP GetMetadata request after discovering MBXHub via UDP Probe; response includes PresentationUrl pointing to /dashboard.

Device Description Contents
FieldDescription
deviceTypeurn:halrad-com:device:MBXHub:1
friendlyNameMBXHub instance identifier
presentationURLDashboard URL (/dashboard)
controlURLREST API base (/api)
eventSubURLWebSocket endpoint (/ws)
Firewall Requirements
PortProtocolPurpose
8080TCPREST API + WebSocket
restPort + 1TCPMetaServer (mood/fingerprint data, proxied through plugin)
1900UDPSSDP (UPnP discovery)
3702UDPWS-Discovery (Windows Network folder)
5353UDPmDNS/DNS-SD (device discovery via raw UDP multicast)

Use the Settings → Firewall panel or CLI: MBXHub.exe firewall add --name MBXHub --tcp 8080,8081 --udp 1900,3702,5353 --urlacl 8080,8081 (8081 = MetaServer, configurable via metaPort).

Configuration: Enable/disable via Settings → "Advertise on local network" checkbox (enabled by default). Settings: 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.

System

GET /status

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.

GET /system/status

Returns system status and enabled modules (JSON)

GET /system/version

Returns API version information. Includes host field with the configured discovery name (or machine name if not set).

GET /system/capabilities

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.2.4",
  "capabilities": ["rest-api", "websocket", "player-control", "autoq", "discovery"],
  "endpoints": {
    "rest": "http://host:8080",
    "ws": "ws://host:8080/ws"
  }
}
GET /ping

Health check endpoint (alias: /system/ping)

GET /system/fleet

Single-pane-of-glass aggregator for distributed scanning. Fans out to the local Shell plus every peer from /meta/sync/status, fetches each node's /meta/health and /meta/stats in parallel (3 s per-node timeout — slow/dead nodes don't block the aggregate). Response is a flat array under data.nodes; each entry is flagged status: up | down | timeout | unknown. Down / timeout entries only carry name, url, status, error. When up, the per-node fields queueDepth, batchSize, scanMode, and scanStatus together describe that node's distributed-scanning posture.

// Response:
{
  "success": true,
  "data": {
    "fetchedAt": "2026-04-19T05:15:00Z",
    "nodeCount": 3,
    "nodes": [
      { "name": "ATOM", "url": "http://127.0.0.1:8081", "status": "up",
        "tracks": 72115, "withFeatures": 70881, "localScanned": 609, "peerSynced": 70272,
        "queueDepth": 0, "batchSize": 1, "scanMode": "auto", "scanStatus": "healthy", "truedatFound": true,
        "syncEnabled": true, "syncing": false, "lastSyncAt": "2026-04-19T04:50:00Z", "lastSyncResult": "3/3 peers, 9 tracks",
        "version": "0.5.2.4", "uptime": "12h34m", "dbSizeMb": 155.2 },
      { "name": "ZIGGY25", "url": "http://192.168.1.82:8081", "status": "up",
        "tracks": 68501, "queueDepth": 3, "batchSize": 5, "scanMode": "local", "scanStatus": "healthy",
        "lastSyncAt": "2026-04-19T04:20:00Z", "dbSizeMb": 128.0 },
      { "name": "OLDBOX", "url": "http://192.168.1.99:8081", "status": "timeout", "error": "3s probe timed out" }
    ]
  }
}
GET /system/topology

Layer 3 fleet topology view. Returns the local node name, every discovered node (local + KnownPeers from /meta/sync/status) with their raw capabilities[] string array (values: meta-server, scanner, peer-sync, smtc, rest-api, websocket, player-control, autoq, discovery — see MBXC/NodeCapabilities.cs for the canonical list), and the current TopologyPreferences block. Per-node probes fan out concurrently; aggregate latency is bounded by the per-node probe timeout rather than summing N sequential timeouts, so one dead peer costs a single timeout window, not the full set. healthy comes from /meta/health (status=="ok" or services.metaServer.status=="running"); reachable is true when we got any successful response from either /meta/capabilities or /meta/health — so a node up-but-degraded shows reachable=true, healthy=false. Access: Shuffle category (mode-aware).

// Response:
{
  "success": true,
  "localNode": "ATOM",
  "discovered": [
    { "name": "ATOM",    "url": "http://127.0.0.1:8081",    "capabilities": ["meta-server","scanner","peer-sync"], "healthy": true,  "reachable": true },
    { "name": "ZIGGY25", "url": "http://192.168.1.82:8081", "capabilities": ["meta-server","scanner"],              "healthy": true,  "reachable": true },
    { "name": "OLDBOX",  "url": "http://192.168.1.99:8081", "capabilities": [],                                         "healthy": false, "reachable": false }
  ],
  "preferences": {
    "metaServerOrder":    [],      // ordered peer URLs for MetaServer lookups (empty = MetaServer.Preferred + SSDP)
    "scanDispatchOrder":  [],      // ordered peer URLs for scan dispatch; "local" accepted (empty = idle-local + round-robin)
    "scanExcludes":       [],      // peer URLs excluded from dispatch (wins over scanDispatchOrder)
    "peerPullFanOut":     []       // ordered peer URLs for peer-pull (empty = PeerSync.KnownPeers as-is)
  },
  "fetchedAt": "2026-04-19T21:45:00Z"
}
PUT /system/topology/preferences

Partial update of TopologyPreferences. Only fields present in the body are modified; absent fields keep their current value. Empty body is rejected (to reset a list, send an explicit empty array: { "metaServerOrder": [] }). Each list capped at 32 entries; each entry must parse as a URI except "local", which is only accepted in scanDispatchOrder. Follows Settings Safety Rule 3: reload-before-save happens atomically inside HubSettings.Mutate() under the settings lock so a concurrent hand-edit of mbxhub.json or a racing SettingsForm save can’t be clobbered. Access: Shuffle category.

// Request — change MetaServer routing only, leave others:
PUT /system/topology/preferences
{ "metaServerOrder": ["http://atom:8081", "http://ziggy25:8081"] }

// Response:
{
  "success": true,
  "preferences": {
    "metaServerOrder":    ["http://atom:8081", "http://ziggy25:8081"],
    "scanDispatchOrder":  [],
    "scanExcludes":       [],
    "peerPullFanOut":     []
  }
}

// Errors:
400 BAD_REQUEST     — empty body, invalid URI, "local" outside scanDispatchOrder, list > 32, non-string element
400 INVALID_JSON    — body not valid JSON
503 SETTINGS_LOCKED — settings path not configured (startup wiring issue; Shell started without a resolvable mbxhub.json)
500 SAVE_FAILED     — Mutate() threw (ReloadFromFile failed, save failed, validation threw) or returned false

Note: non-PUT methods on this path return 404 rather than 405 — only PUT is registered with the router, so fall-through lands on the generic not-found handler. The handler’s internal method check is defensive and unreachable in practice.

Honor points (what each list actually affects): metaServerOrderHubController.ResolveMetaServer preflight (before MetaServer.Preferred / SSDP). scanDispatchOrderScannerPool.Dispatch ordering (before idle-local + round-robin). scanExcludesScannerPool.Dispatch filter (applied first). peerPullFanOutPeerPullService.TryPullFeaturesByFingerprintAsync peer order. All empty = zero behavior change; pre-Layer-3 paths unchanged.

GET /system/fleet/queue?node=NAME&limit=N

Per-node queue snapshot. Proxies to the resolved Shell’s GET /meta/queue (same-origin from the plugin, avoids cross-origin blocks in a multi-host fleet). node identifies which fleet entry to query; limit default 20, max 100. Returns the same shape as /meta/queue.

GET /system/fleet/activity?node=NAME&since=SEQ&limit=N

Per-node scan activity feed. Proxies to the resolved Shell’s GET /meta/activity. Incremental via since — clients re-poll with the previous response’s currentSeq to receive only new events. limit default 50, max 200. First poll with since=0 seeds the cursor and returns the last N events.

// Response:
{
  "workerStarted": true,
  "currentSeq": 1847,
  "returned": 3,
  "events": [
    { "seq": 1845, "at": "2026-04-19T23:55:22Z", "kind": "complete", "file": "C:\\Music\\A\\1.mp3", "basename": "1.mp3", "elapsedMs": 412, "error": null, "dispatchedTo": "ATOM" },
    { "seq": 1846, "at": "2026-04-19T23:55:23Z", "kind": "start",    "file": "C:\\Music\\A\\2.mp3", "basename": "2.mp3", "dispatchedTo": "ZIGGY25" },
    { "seq": 1847, "at": "2026-04-19T23:55:26Z", "kind": "failed",   "file": "C:\\Music\\A\\3.mp3", "basename": "3.mp3", "elapsedMs": 2103, "error": "truedat timeout", "dispatchedTo": "ZIGGY25" }
  ]
}

Event kinds: start, complete, failed. dispatchedTo is self-reported by the executing ScanWorker (Environment.MachineName) so fleet-aggregated feeds show which node actually ran each scan — useful for visualizing dispatch flow when one peer routes work to another.

GET /system/features

Returns enabled feature flags: banlist, ratings, loved, reactions, streaming, autoq. Clients use these to show/hide UI elements.

GET /system/settings PUT

Get or update hub configuration (ports, enabled modules). restPort and restEnabled changes require localhost (403 for remote). All changes blocked for remote clients during party mode.

GET /system/settings/schema

Returns schema for all configurable settings. Each entry includes:

FieldDescription
keyDotted key path (e.g. autoQ.batchSize)
typebool, int, double, string, enum
categoryGrouping: General, AutoQ, Scoring, Dashboard, API, etc.
tierStandard, Advanced, or Expert
descriptionHuman-readable explanation
defaultDefault value
currentCurrent live value
min, max, stepRange constraints (numeric types only)
optionsValid values (enum types only)
requiresRestartWhether 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.

PUT /system/config POST

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 /system/default-page PUT

Get or set the default redirect page (body: {"defaultPage":"/pages/player.html"})

GET /system/theme PUT

Get or update the unified theme configuration. Two configurable mode slots with HSL color values. Partial updates supported — only include the fields you want to change.

// GET response:
{
  "activeMode": 1,
  "mode1": { "accentHue": 245, "bgHue": 245, "bgLightness": 97, "textLightness": 15 },
  "mode2": { "accentHue": 245, "bgHue": 245, "bgLightness": 7, "textLightness": 90 },
  "active": { "accentHue": 245, "bgHue": 245, "bgLightness": 97, "textLightness": 15 }
}

// 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

PUT broadcasts a ThemeChanged WebSocket event and sets the mbxh_theme cookie for dashboard SSR.

GET /system/metrics

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"
  }
}
GET /system/uptime

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"
  }
}
GET /system/qr

Generate QR code PNG image for the MBXHub base URL. Optional ?url= for custom target.

GET /system/client-ip

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.

POST /system/seed-resources

Force re-extract all embedded resources (pages and charms) to disk. Overwrites existing files.

// Response:
{
  "success": true,
  "data": {
    "pagesUpdated": 8,
    "charmsUpdated": 3,
    "userModified": []
  }
}
POST /system/client-log

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).

Player Control

Playback
POST/player/play POST/player/pause POST/player/playpause POST/player/stop
Navigation
POST/player/next POST/player/previous POST/player/next-album POST/player/previous-album
Volume & Position
GET/player/volume PUT/player/volume GET/player/mute PUT/player/mute GET/player/position PUT/player/position
Shuffle & Repeat
GET/player/shuffle PUT/player/shuffle GET/player/repeat PUT/player/repeat
Audio Processing
GET/player/equalizer PUT/player/equalizer GET/player/dsp PUT/player/dsp GET/player/crossfade PUT/player/crossfade GET/player/replaygain PUT/player/replaygain
AutoDJ & Advanced
GET/player/autodj POST/player/autodj/start POST/player/autodj/stop GET/player/stopaftercurrent POST/player/stopaftercurrent
Output Devices
GET/player/output-devices PUT|POST/player/output-device
Scrobbling
GET/player/scrobble PUT/player/scrobble
Status & Display
GET/player/status GET/player/button-enabled GET/player/show-time-remaining GET/player/show-rating-track GET/player/show-rating-love POST/player/show-equaliser
Streaming & Statistics
POST/player/queue-random POST/player/open-stream POST/player/update-play-statistics

Now Playing

GET /nowplaying

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
  }
}
Track Data & Metadata
GET/nowplaying/position GET/nowplaying/tag GET/nowplaying/property

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"}
Artwork
GET/nowplaying/artwork GET/nowplaying/artwork-url GET/nowplaying/downloaded-artwork GET/nowplaying/downloaded-artwork-url

Returns album artwork as binary image or URL

Lyrics
GET/nowplaying/lyrics GET/nowplaying/downloaded-lyrics
Artist Pictures
GET/nowplaying/artist-picture GET/nowplaying/artist-thumbnail GET/nowplaying/artist-picture-urls GET/nowplaying/artist-pictures/{index}

/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).

Soundtrack
GET/nowplaying/is-soundtrack GET/nowplaying/soundtrack-pictures
Audio Analysis
GET/nowplaying/spectrum GET/nowplaying/sound-graph GET/nowplaying/sound-graph-ex

FFT spectrum and waveform data for visualizations

Peak Metering
GET/nowplaying/peak

Current stereo peak and RMS levels (0.0–1.0). Returns {peak: [L, R], rms: [L, R]}. Requires MusicBee 3.6+ (API rev 58+).

Queue Management

GET /queue

Returns the now playing list. Supports ?offset=0&limit=50

// 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
      },
      // ... more tracks
    ]
  }
}
Queue Info
GET/queue/current GET/queue/next-index GET/queue/has-prior GET/queue/has-following
Queue Actions
POST/queue/add POST/queue/playnow POST/queue/play POST/queue/clear POST/queue/move POST/queue/play-library-shuffled
// 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}
Track at Index
GET/queue/{index}/url GET/queue/{index}/tag GET/queue/{index}/property DELETE/queue/{index}

Library

GET /library/files

Query library with ?query=, ?artist=, ?albumArtist=, ?album=, ?genre=, ?sort=

// 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
    ]
  }
}
GET /library/search

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:

  • strict (default) — each query word must be a prefix of a real word in the text. "St Anger" does NOT match "Stranger."
  • substring — loose matching, any query word appearing anywhere passes. "anger" matches "Stranger."

The default mode is controlled by the Library.DisableSubstringSearch setting (true by default = strict). Override per-call with ?substring=true|false. The response payload includes a mode field echoing which mode was used.

Adjacent-token matching (v0.5.2.3) — in strict mode, multi-word queries also require the tokens to match consecutive word prefixes in a single field (title, artist, album, or genre). "St Anger" matches the album St. Anger but not a track whose title contains "Anger" while its album contains "Story." Controlled by Library.DisableCrossFieldMatching (true by default). Set to false to restore the legacy cross-field behavior. Single-word queries are unaffected. Ignored when substring mode is active.

Aggregations
GET/library/artists GET/library/album-artists GET/library/albums GET/library/albums/detailed GET/library/albums/unheard GET/library/albums/with-pdf GET/library/genres GET/library/inbox GET/library/audiobooks GET/library/videos

/library/albums/detailed returns albums with firstTrackUrl, year, and dateAdded for artwork lookups and sorting. 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. Both 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).

GET /library/evaluate

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). Requires MusicBee 3.4.1+ (API rev 55+).

GET /library/no-artwork

MusicBee’s built-in placeholder image for tracks with no artwork. Returns binary image data with appropriate content type. Cache-Control: 24h. Requires MusicBee 3.5+ (API rev 57+).

GET /library/albums/by-artist

Albums for an artist with year, track count, and firstTrackUrl for artwork. 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"
      }
    ]
  }
}
File Operations
GET/library/file/{url} PUT/library/file/{url} POST/library/add POST/library/artwork/batch POST/library/find-device-ids POST/library/sync-delta POST/library/commit

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}}
File Details
GET/library/file/{url}/lyrics GET/library/file/{url}/artwork GET/library/file/{url}/artwork-url GET/library/file/{url}/artwork-count GET/library/file/{url}/pdf GET/library/file/{url}/has-pdf GET/library/file/{url}/device-id PUT/library/file/{url}/device-id

/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.

Fan Art (Folder Images)
GET /library/file/{url}/fan-art

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.

GET /library/fan-art/{path}

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.

Artist Info
GET/library/artist/{name}/similar GET/library/artist/{name}/picture GET/library/artist/{name}/thumbnail GET/library/artist/{name}/pictures GET/library/artist/{name}/pictures/{index}

/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).

Play History
GET/library/recent

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 Library
GET/library/videos

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}.

Diagnostic
GET/library/files/raw GET/library/cuetest

/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=

GET /radio/stations

List radio stations from MusicBee’s Radio node.

// Response:
{
  "success": true,
  "data": {
    "total": 5,
    "stations": [
      {"url": "http://stream.example.com/radio", "name": "Jazz FM"},
      // ...
    ]
  }
}

Playlists

List & Create
GET/playlists POST/playlists
// 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}}
Playlist Operations
GET/playlists/{url} PUT/playlists/{url} DELETE/playlists/{url}
// PUT /playlists/{url} - Replace playlist contents
{"files": ["file:///C:/Music/song1.mp3", "file:///C:/Music/song2.mp3"]}
Playlist Files
GET/playlists/{url}/files POST/playlists/{url}/files POST/playlists/{url}/play
// 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"]}

Pending Files

GET/pending GET/pending/url GET/pending/tag GET/pending/property

Podcasts

GET /podcasts

List podcast subscriptions. Query: ?query=

Subscription Details
GET/podcasts/{id} GET/podcasts/{id}/artwork GET/podcasts/{id}/episodes GET/podcasts/{id}/episodes/{index}

/podcasts/{id}/episodes/{index} — get a specific episode by numeric index.

GET /podcasts/episodes

Flat episode listing. Query: ?id= (feed URL). Use this when the subscription ID is a URL rather than a simple ID.

MusicBee Settings

Read-only passthroughs to MusicBee’s own settings API. For MBXHub’s configurable hub settings, see /system/settings above.

GET /settings

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
  }
}
Individual Settings
  • GET /settings/storage-path — Persistent storage path
  • GET /settings/skin — Current skin name
  • GET /settings/skin-element-color — Skin color. Query: ?element=SkinSubPanel&state=ElementStateDefault&component=ComponentBackground
  • GET /settings/window-borders-skinned — Whether window borders are skinned
  • GET /settings/lastfm-user — Last.fm user ID
  • GET /settings/web-proxy — Web proxy configuration
Field & Data Info
  • GET /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 field
  • GET /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 pair

MusicBee Application

POST /app/exit

Gracefully 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
  }
}
POST /app/restart

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)
}
Window Control
GET/mb/window-handle POST/mb/window-size POST/mb/refresh-panels
Commands
POST/mb/command POST/mb/filter POST/mb/nowplaying-assistant POST/mb/download
Localisation
GET/mb/localisation
Visualizers & Plugins
GET/mb/visualisers POST/mb/visualiser GET/mb/plugin-views POST/mb/plugin-view

PartyMode

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)

Party Session
GET /partymode/status

Get current party state (active, request count).

POST /partymode/start

Start a party session.

// Request body:
{"guestPin": "1234", "djPin": "5678"}
// djPin is optional - defaults to guestPin
POST /partymode/stop

End the current party session.

Guest Access
GET /partymode/validate?pin=1234&nickname=Haro

Validate PIN and get role. If nickname provided, announces join in feed.

// Response:
{"success": true, "data": {"valid": true, "role": "guest"}}
// role: "guest" or "dj"
POST /partymode/verify-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
POST /partymode/vote

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"
POST /partymode/request

Submit a song request (adds to queue).

// Request body:
{"url": "C:\\Music\\song.mp3", "nickname": "Haro"}
// Response includes requestId, title, artist
Feed & Display
GET /partymode/feed?limit=20

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 /partymode/requests?limit=20

Get recent song requests only (for DJ page).

GET /partymode/qr

Generate QR code PNG image. Auto-includes PIN if party is active.

GET /partymode/role

Returns the caller's role based on IP: host (loopback), dj (verified DJ PIN), or guest.

Settings

Party Mode settings are configured in MusicBee via Settings → Network → Party Mode...

SettingDefaultDescription
protectMetadatatrueBlock metadata writes (love, rate, tag edits) for ALL users including DJ
rateLimitEnabledtrueEnable per-IP rate limiting on party endpoints
rateLimitRequestsPerMinute5Max song requests per minute per IP (when rate limiting enabled)
rateLimitVotesPerMinute5Max votes per minute per IP (when rate limiting enabled)
rateLimitPinAttemptsPerMinute5Max PIN validation attempts per minute per IP
trustForwardedForfalseTrust 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.

Web Pages

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 vibes
  • dj.html - Start/stop party, set PINs, manage queue, see requests & vibes
  • display.html - TV mode: artwork, lyrics, request feed, QR code, floating reactions
  • leaderboard.html - Party stats: guest activity, top tracks, reaction counts

AutoQ

Intelligent queue system. AutoQ combines TrueShuffle rules, mood analysis, reactions, and influences to automatically queue tracks that match the room's energy.

TrueShuffle (Rules Engine)

TrueShuffle manages the shuffle cycle — play rules, cycle tracking, and queue constraints. Returns 503 SERVICE_UNAVAILABLE if TrueShuffle/AutoQ not enabled.

GET /shuffle/status

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"
  }
}
POST/shuffle/reset

Reset the shuffle cycle. All tracks become unplayed.

GET/shuffle/played GET/shuffle/remaining

Tracks played/remaining in shuffle cycle. Query: ?offset=&limit=

Banlist

Permanently excluded tracks. Banned tracks are never queued by AutoQ. Returns 503 SERVICE_UNAVAILABLE if TrueShuffle/AutoQ not enabled.

GET /banlist

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"
      }
    ]
  }
}
Manage Banlist
POST/banlist DELETE/banlist/{url}
// 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)

Influences

Influence rules shape AutoQ scoring — Pandora-style thumbs up/down on artists and genres. Unlike bans (permanent, track-specific), influences are resettable metadata preferences. Returns 503 if TrueShuffle/AutoQ not enabled.

Negative (--): Hard exclude matching tracks. Positive (++): Preference boost (future).

GET /influences

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 /influences/current

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": "++"
  }
}
Manage Influences
POST/influences DELETE/influences/{target}/{value} POST/influences/clear
// 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)

Scoring & Vibe List

How it works: AutoQ maintains a scored list of candidate tracks (the "vibe list"). Scores are based on:
  • Reactions: Now playing reactions from guests (fire +3, heart +2, like +1, dislike -1, ban -100). Reactions also create influences automatically: fire/heart → positive artist influence, like → positive genre, dislike → negative genre, ban → negative artist.
  • Influences: Thumbs up/down on artists and genres from party voting and reactions
  • Recency: Small boost for recently reacted tracks
When the queue runs low, AutoQ picks top-scoring tracks to add.
AutoQ Control
GET /autoq/status

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 }
    ]
  }
}
POST /autoq/start

Start AutoQ. Begins monitoring queue and adding tracks when needed.

// Optional request body:
{ "mode": "autopilot" }
// Modes: "autopilot" (default), "djassist" (aliases: "dj", "assist", "auto")
POST /autoq/stop

Stop AutoQ. Queue continues playing but no new tracks are added automatically.

POST /autoq/reset

Reset AutoQ session state (clears reactions, taste vector, ban list). DJ-only in party mode.

POST /autoq/vibe-list/refresh

Force refresh vibe list. Returns updated track count.

// Response:
{
  "success": true,
  "data": { "message": "Vibe list refreshed", "count": 100 }
}
POST /autoq/pick

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" }
}
POST /autoq/unban

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" }
}
GET /autoq/banned

Check if the currently playing track is banned.

// Response:
{
  "success": true,
  "data": { "url": "C:\\Music\\track.mp3", "isBanned": false }
}
Mood Channels
GET /autoq/moods

Get available mood channels with arousal/valence coordinates. Channels are customizable via autoQ.moodChannels in mbxhub.json.

// Response:
{
  "success": true,
  "data": {
    "currentMood": "Energetic",
    "channels": [
      { "name": "Energetic", "emoji": "🔥", "arousal": 0.90, "valence": 0.80 },
      { "name": "Chill", "emoji": "😌", "arousal": 0.35, "valence": 0.65 }
    ]
  }
}
GET /autoq/moods/browse

Browse tracks matching a mood channel. Returns scored tracks sorted by mood similarity.

channelMood channel name (e.g. "Energetic", "Chill")
limitMax 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 }
    ]
  }
}
GET /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.

// Response (Essentia-analyzed track):
{
  "success": true,
  "data": {
    "file": "C:\\Music\\Artist\\Album\\Track.mp3",
    "album": "Album",
    "valence": 0.5003,
    "arousal": 0.5794,
    "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
  }
}
POST /autoq/mood

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 }
  }
}
POST /autoq/moods/reload

Reload the mood channel cache from disk (re-reads Essentia data).

GET /autoq/mood-cache/status

MetaServer mood-cache warm-up visibility. Returns current cache totals plus the most recent warm-up summary. lastWarmup is null when no warm-up has run yet (e.g. MetaServer disabled). For push updates, subscribe to the MoodCacheWarmed WebSocket event instead of polling.

// Response:
{
  "success": true,
  "data": {
    "total": 12480,
    "essentia": 9812,
    "fallback": 2668,
    "warmupInProgress": false,
    "lastWarmup": {
      "at": "2026-04-18T00:15:42Z",
      "ran": true,
      "skipReason": null,
      "librarySize": 12480,
      "fromLocalFile": 0,
      "fromMetaServer": 9812,
      "stillMissing": 2668,
      "durationMs": 1843
    }
  }
}
POST /autoq/retag-moods

Bulk write mood tags to a MusicBee custom field. Only processes Essentia-analyzed tracks. Requires autoQ.moodTagField to be configured (e.g. "Custom1").

// Response:
{
  "success": true,
  "data": { "updated": 342 }
}
Vibe List
GET /autoq/vibe-list

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
      }
    ]
  }
}
Taste Explorer
GET /autoq/taste-explorer

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
  }
}
GET /autoq/similar

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"]
    }]
  }
}
Reactions

Tiered reactions for the now playing track. Each emoji has a different score weight.

EmojiNameScoreDescription
🔥fire+3This track is fire! (triggers queue refresh)
❤️heart+2Love this song
👍like+1Good choice
👎dislike-1Not feeling it
🚫ban-100Skip and exclude from queue
POST /autoq/react

Submit a reaction for a track. Reactions appear as floating emojis on the Display page.

// 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 /autoq/reactions

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"
      }
    ]
  }
}
Leaderboard
GET /autoq/stats

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 /autoq/settings

Get all tunable AutoQ parameters: scoring weights, reaction scores, influence scores, estimation weights, and normalization ranges. Use with Tuning Console.

PUT /autoq/settings

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.

// Example: change just the BPM arousal weight
PUT /autoq/settings
{"estimation": {"arousalWeightBpm": 0.30}}
Configuration

AutoQ settings in mbxhub.json under the autoQ section:

SettingDefaultDescription
enabledfalseEnable AutoQ feature
pickMode"weighted"Track selection mode: off (disabled, shuffle fill only), favorites (always pick highest-scored), weighted (score-proportional random from top candidates), random (uniform random, diversity caps still apply)
queueThreshold3Add tracks when queue drops below this
batchSize5Tracks to add per batch
vibeListSize100Size of candidate track pool
moodMatchWeight0.4Weight for mood matching in scoring (0-1)
recencyDecayLambda0.1Decay rate for recency boost on reacted tracks
recencyPenaltyDecay0.1Decay rate for recently-played penalty
minReplayMinutes30Minimum minutes before a track can be replayed
diversityWindowSize10Recent tracks considered for diversity calculations
minSessionEntropy0.5Entropy threshold before boosting diversity (0-5)
vibeListRefreshMinutes30Minutes between automatic vibe list refreshes
moodChannelsnullCustom mood channels (array). Uses defaults if null.
genreQuota3Max consecutive same-genre tracks (0=disabled)
artistQuota1Max tracks from same artist in batch (0=disabled)
moodTagField"Custom1"MusicBee custom tag field for mood labels (null=disabled)
moodTagFieldName"AutoQ Mood"Expected display name for the tag field (must match MusicBee config)

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.

QuadrantProfileTypical GenresAcoustic Traits
High arousal + high valenceEnergetic, upbeatEDM, pop, funkFast tempo, bright timbre, strong beats
High arousal + low valenceTense, aggressiveMetal, hard rock, industrialDistortion, high energy, dissonance
Low arousal + low valenceSad, subduedAmbient drone, slow blues, lo-fiSlow tempo, dark timbre, soft dynamics
Low arousal + high valenceCalm, pleasantChillhop, acoustic folk, soft jazzWarm timbre, consonance, smooth textures

Auto Mood Scan

Automatic Essentia analysis pipeline. When a track changes, the plugin checks if the current and upcoming queue tracks have Essentia data. Missing tracks are broadcast as ScanEnqueue WebSocket events. The Shell's ScannerPool routes each request to the appropriate scanner (local ScanWorker or remote MetaServer), runs truedat.exe --analyze-file, and posts results to MetaServer ingest. Self-disabling if Truedat/Essentia not found and no remote scanner is configured.

Auto-scan flow: TrackChanged → plugin broadcasts ScanEnqueue (priority: high for current track, normal for look-ahead) → Shell ScannerPool → ScanWorker or remote → truedat.exe → MetaServer ingest.

Plugin config (mbxhub.json → autoScan): enabled (default true), lookAhead (default 5 tracks).
Shell scan config (mbxhub-shell.json → scan): mode (off|local|auto, default auto), includeLocal (default true), preferredUrl (null = no preference), batchSize (default 1), cpuLimit (default 25%). Readable and writable via GET/PUT /meta/config.

Scan modes: off disables all scanning. local forces local ScanWorker only. auto uses ScannerPool — tries preferredUrl first, falls back to local if includeLocal is true.
Remote scanning: Set syncLibraryPath + syncSharePath in mbxhub.json. ScanEnqueue events include a share field with the UNC path. Remote Shell instances open files via UNC, scan with Essentia, and deliver features back via PeerSync. Scanner requires zero path configuration.
GET /scan/status

Mood cache statistics and auto-scan configuration.

Response: {
  "success": true,
  "moodCache": { "total": 70000, "essentia": 45000, "fallback": 25000 },
  "autoScan": { "enabled": true, "lookAhead": 5 }
}
POST /scan/unanalyzed

Bulk-enqueue unanalyzed library tracks for background Essentia scanning. Broadcasts ScanEnqueue WebSocket events for the Shell’s ScanWorker to pick up. Responds 200 immediately with { "success": true, "enqueuing": true, "totalFiles": N }; the enumerate-and-broadcast loop runs on a ThreadPool task so the plugin HTTP thread doesn’t stall on large libraries.

POST /scan/track

Enqueue a single track or album for Essentia analysis. Does not require autoScan.enabled. Body: { "file": "C:\\path\\to\\track.mp3" } and/or { "album": "Album Name" }. Broadcasts ScanEnqueue events. Response: { "success": true, "enqueued": N }

POST /scan/fingerprints

Phase 2 gap-detection sweep. Enumerates the library, queries MetaServer /meta/identity-coverage in 200-path chunks to find tracks missing identity.fingerprint.v1, and enqueues a ScanEnqueue(mode=fingerprint) for each. Idempotent — already-fingerprinted tracks are skipped. Cheap because it runs truedat in hash-only mode (no Essentia); tracks only get the 8 fingerprint.v1 sub-keys, enabling ms-scale peer probes via /meta/by-fingerprint. Responds 200 immediately with enqueuing:true + totalFiles; the coverage-probe loop and per-missing broadcasts run on a ThreadPool task with periodic yields so the plugin HTTP thread isn’t stalled for tens of seconds on large libraries (see log line HandleFingerprints background sweep for final counts). 503 if MetaServer is not resolved.

Body (optional): { "mode": "fingerprint" }   // or "stream" for audioStreamSha256 sweep
Response (immediate): { "success": true, "enqueuing": true, "totalFiles": 72115, "mode": "fingerprint" }
Empty library:        { "success": true, "enqueued": 0, "totalFiles": 0 }
Log (on completion):  HandleFingerprints background sweep mode=fingerprint scanned=72115/72115 missing=71506 enqueued=71506 elapsedMs=8234

Library Sync

Status & Discovery
GET/sync/status GET/sync/peers GET/sync/discover
Delta & Operations
GET/sync/delta GET/sync/operations GET/sync/operations/{syncId}
Sync Actions
POST/sync/start POST/sync/stop POST/sync/pull POST/sync/push

Device Sync

POST/mbsync/file/start POST/mbsync/file/end POST/mbsync/file/delete/start POST/mbsync/file/delete/end

ARiA - Input Simulation

Simulate keyboard and mouse input to wake or control the host PC. Useful for remote wake scenarios. Full ARiA Documentation →

Security: ARiA is disabled by default (ariaEnabled: false). Returns 403 ARIA_DISABLED when disabled.
GET /aria/status

Check if ARiA input simulation is enabled

GET POST /aria/wake

Quick wake: move mouse + send Shift key to wake sleeping/locked PC

POST /aria/send-keys

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).

POST /aria/focus

Focus a window by title. Body: {"window": "Notepad"} (partial match, case-insensitive)

Mouse Control
POST/aria/mouse/move POST/aria/mouse/click

Move: {"x":100,"y":100} (absolute) or {"dx":10,"dy":0} (relative)

Click: {"button":"left"} or {"x":500,"y":300,"button":"right"}

Presets & Programs
GET/aria/presets

List available presets

GET/aria/preset/{name}

Execute a preset by name (e.g., /aria/preset/RIA3)

GET/aria/programs

List allowed programs for the run() command. Returns names only (paths not exposed).

Customizing Presets: Edit %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/-3
run(name[,extraArgs]) - Launch a pre-configured program: run(amp-on) or run(visualizer,--fullscreen). Programs must be defined in ariaAllowedPrograms in mbxhub.json
webhook(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 notification
restart(target) - Restart: mb (MusicBee), system, or shutdown
Chain commands: sndkeys(^a);delay(100);sndkeys(^c)
Allowed Programs (run command): The run() command only launches programs defined in ariaAllowedPrograms. Empty by default.
"ariaAllowedPrograms": [
  {"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).

RemoteApp

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.

GET /remoteapp/status

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."
}
GET /remoteapp/rdp

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)
  • Any other query parameter is forwarded as an .rdp setting override (e.g. 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.

Setup (CLI): Machine configuration is done via the CLI:
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)
Prerequisites — Windows Client (Pro/Enterprise):
1. Settings → System → Remote Desktop → turn ON
2. Allow through firewall (Windows usually prompts automatically).
   If not: Control Panel → Windows Defender Firewall → Allow an app → Remote Desktop → check both boxes.
3. Network Level Authentication (NLA) is on by default. Disable it if older clients cannot connect.
Prerequisites — Windows Server:
Install the Remote Desktop Services role:
Server Manager → Add Roles and Features → Remote Desktop Services →
• Remote Desktop Session Host
• Remote Desktop Connection Broker
• Remote Desktop Web Access

Or via PowerShell:
Install-WindowsFeature RDS-Connection-Broker, RDS-Web-Access, RDS-RD-Server -IncludeManagementTools

Device Proxy

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.

POST /api/proxy

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 PUT
  • url — 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:

  • 400 — Missing or invalid request body, missing method/url fields
  • 403 — Target URL is not a private IP (public internet proxying is blocked)
  • 405 — Only POST is accepted on this endpoint
  • 502 — Target device unreachable or returned an error
Security: The proxy only forwards to RFC 1918 private IP ranges (10.x, 172.16–31.x, 192.168.x) plus loopback (127.x, localhost). Requests to public IPs are rejected with 403. Timeout is 5 seconds.
Request flow:
Browser → POST /api/proxy → MBXHub → LAN device
Browser ← JSON response (passthrough) ← MBXHub ← LAN device

The 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.

Audio Streaming

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.

GET /stream/{path}

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.

Security:

  • Path traversal (..) is blocked
  • Only whitelisted audio extensions are served
  • File must exist in the MusicBee library (verified via API)

Errors:

  • 400 — Invalid path or non-audio extension
  • 403 — File not in MusicBee library
  • 404 — File not found or streaming disabled
  • 416 — Range not satisfiable
CUE tracks: Supported. The player seeks to the correct offset and shows track-relative progress. Track boundaries are enforced (auto-advance at track end).

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.

Media

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.

Settings

Configure in mbxhub.json under media:

"media": {
  "imageRoot": "C:\\Users\\...\\Pictures\\Wallpapers",
  "videoRoot": "C:\\Users\\...\\Videos",
  "intervalSeconds": 30,
  "shuffle": true
}
GET /media/settings

Returns timer config: {intervalSeconds, shuffle}

Categories

GET /media/images/categories

List subfolder names under imageRoot. Files directly in root appear as _root.

GET /media/videos/categories

List subfolder names under videoRoot.

File Listing

GET /media/images/{category}

List filenames in category. Returns {name, files:[], count}. Filenames only, no paths.

GET /media/videos/{category}

List video filenames in category.

Rotation

GET /media/images/{category}/next

Serve the next image (rotation state per category, sequential or shuffled). Returns image binary.

GET /media/videos/{category}/next

Serve the next video with HTTP Range support for seeking.

Direct File Access

GET /media/images/{category}/file/{name}

Serve a specific image by filename.

GET /media/videos/{category}/file/{name}

Serve a specific video by filename. Supports HTTP Range requests (206 Partial Content) for seeking.

Supported Formats

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).

Security

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.

Page

/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.

Device & Mixer Control

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.

Windows Audio Device

GET /devices/audio/outputs

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 /devices/audio/volume

Get Windows audio device volume and mute state

// Response:
{
  "device": "Speakers (HD Audio)",
  "volume": 75,
  "muted": false
}
PUT /devices/audio/volume

Set Windows audio device volume (0–100)

PUT /devices/audio/volume
Content-Type: application/json

{ "volume": 50 }
PUT /devices/audio/mute

Set Windows audio device mute state

PUT /devices/audio/mute
Content-Type: application/json

{ "mute": true }

Network Endpoints

Manage network audio endpoints (speakers, receivers). Endpoints are saved in settings and controlled via REST. Currently supports Devialet Phantom speakers.

GET /devices/endpoints

List all configured network endpoints

// Response:
[
  {
    "id": "devialet-1",
    "name": "Living Room",
    "type": "Devialet",
    "ip": "192.168.1.50"
  }
]
POST /devices/endpoints

Add a new network endpoint

POST /devices/endpoints
Content-Type: application/json

{
  "ip": "192.168.1.50",
  "type": "devialet",
  "name": "Living Room"
}
POST /devices/endpoints/scan

Scan local network for speakers via mDNS/DNS-SD. Sends PTR queries for _devialet, _phantom, _http, and _airplay service types. Runs a 4-second multicast browse. Returns all discovered devices.

// Response:
[
  {
    "name": "Phantom Premier",
    "ip": "192.168.1.50",
    "port": 80,
    "hostName": "Phantom-Premier.local.",
    "alreadyConfigured": false,
    "existingId": null
  }
]
DELETE /devices/endpoint/{id}

Remove a saved endpoint

GET /devices/endpoint/{id}/volume

Get endpoint volume and mute state

// Response:
{
  "volume": 40,
  "muted": false
}
PUT /devices/endpoint/{id}/volume

Set endpoint volume (0–100)

PUT /devices/endpoint/{id}/volume
Content-Type: application/json

{ "volume": 50 }
PUT /devices/endpoint/{id}/mute

Set endpoint mute state

PUT /devices/endpoint/{id}/mute
Content-Type: application/json

{ "mute": true }
GET /devices/endpoint/{id}/sources

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 }
]
PUT /devices/endpoint/{id}/source

Select an input source on the endpoint

PUT /devices/endpoint/{id}/source
Content-Type: application/json

{ "sourceId": "upnp" }

Mixer Settings

Configure which fader the dashboard volume controls target by default.

GET /mixer/settings

Get mixer settings

// Response:
{
  "defaultFader": "player"
}
PUT /mixer/settings

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 /mixer/volume

Get current volume from the active default fader (player or device). Returns fader type and volume (0-100).

// Response (player fader):
{ "fader": "player", "volume": 75 }

// Response (device fader):
{ "fader": "device", "volume": 80, "muted": false, "device": "Speakers" }
PUT /mixer/volume

Set volume on the active default fader. Also accepts POST.

PUT /mixer/volume
Content-Type: application/json

{ "volume": 80 }

Charms

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.

Manifest Format

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" }
  ]
}

Settings

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" }
  }
}

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.

MetaServer

Mood feature and identity database served by the Shell process (MBXHub.exe) on restPort + 1. Backed by MBXU SQLite. Accepts scan results from distributed scanners and serves mood data to the plugin.

Transparent proxy: The plugin proxies all /meta/* requests to MetaServer — clients use the same base URL, no separate port needed. MetaServer starts independently of MusicBee and can serve imports and queries from cold start.
Health & Discovery
GET /meta/health

Full system health. Returns shell version, service status, feature readiness, tool discovery results, MetaServer database state, peer sync state, and scan queue depth. Tool entries include path and version when found, or searchedPaths when not found. Scan section includes mode, batchSize, healthy, and queueDepth.

Response: {
  "status": "ok", "version": "1.0", "database": "ready",
  "nodeId": "mbxhub-DF60-8080", "name": "PANDABOX",
  "core": { "shellVersion": "0.5.2.4", "uptime": "00:05:32", "pluginConnected": true },
  "services": { "metaServer": "running", "metaPort": 8081, "trackCount": 68500 },
  "features": { "scan": "ready", "peerSync": "enabled" },
  "scan": { "mode": "auto", "batchSize": 1, "healthy": true, "queueDepth": 3 },
  "tools": {
    "truedat": { "status": "found", "path": "C:\\...\\truedat.exe", "version": "1.2.0" },
    "ffmpeg":  { "status": "not_found", "searchedPaths": ["C:\\...\\ffmpeg.exe", "..."] },
    "essentia": { "status": "found", "path": "C:\\...\\streaming_extractor_music.exe" }
  }
}
POST /meta/discover

Re-run tool discovery and refresh health status. No request body needed.

Response: { "status": "ok", "message": "Discovery refreshed" }
GET /meta/capabilities

Node capabilities for cross-instance SSDP discovery. Used by PeerSync and the plugin to identify what this Shell instance offers. Capabilities include meta-server, scanner (if Truedat/Essentia found), smtc, and peer-sync. Also returns scan details: mode, batchSize, healthy, queueDepth.

Response: {
  "node": "shell", "version": "0.5.2.4.0",
  "capabilities": ["meta-server", "scanner", "smtc", "peer-sync"],
  "endpoints": { "meta": "http://pandabox:8081" },
  "scan": { "mode": "auto", "batchSize": 1, "healthy": true, "queueDepth": 0 }
}
GET /meta/stats

Database statistics: track counts, mood feature coverage by source, identity signal counts, average confidence, database size, and last import info. The bySource dictionary keys depend on what's ingested your DB: essentia (Truedat scan), essentia-local (older Truedat), peer-sync (federated from another MBXHub), ab-catalog (AcousticBrainz import), metadata-match (identity-only record, no features). Clients should sum any non-peer-sync + non-ab-catalog keys for a “local-scan” rollup.

Response: {
  "success": true,
  "data": {
    "totalTracks": 72115,
    "withMoodFeatures": 70881,
    "bySource": { "essentia": 609, "peer-sync": 70272 },
    "identitySignals": { "audioMd5": 0, "chromaprint": 0, "metadataKey": 70271 },
    "averageConfidence": 0.95,
    "databaseSizeMb": 155.2,
    "lastImport": { "source": "mbxmoods.json", "completedAt": "2026-04-18T12:05:00Z", "tracksImported": 609 }
  }
}
GET /meta/queue?limit=N

Snapshot of pending ScanWorker items in priority order. Operator visibility — Fleet panel consumes this via the /system/fleet/queue proxy. Returns 200 with empty array when the worker is absent or idle; never 503 (“no worker” and “empty queue” render the same way). limit default 20, max 100.

Response: {
  "workerStarted": true, "queueDepth": 147, "returned": 20,
  "items": [
    { "file": "C:\\Music\\A\\1.mp3", "basename": "1.mp3", "share": null, "priority": "high" },
    { "file": "C:\\Music\\A\\2.mp3", "basename": "2.mp3", "share": "\\\\NAS\\Music", "priority": "normal" }
  ]
}
GET /meta/activity?since=SEQ&limit=N

Recent scan start/complete/failed events newer than since. 200-event ring buffer with monotonic sequence cursor; clients advance since = currentSeq each poll to receive only new events. First poll with since=0 seeds the cursor AND returns the last N events. limit default 50, max 200. Each event includes dispatchedTo (Environment.MachineName of the executing worker) so fleet-aggregated feeds show which node actually ran each scan. Proxied plugin-side via /system/fleet/activity.

Response: {
  "workerStarted": true, "currentSeq": 1847, "returned": 3,
  "events": [
    { "seq": 1845, "at": "2026-04-19T23:55:22Z", "kind": "complete", "file": "...", "basename": "1.mp3", "elapsedMs": 412, "error": null, "dispatchedTo": "ATOM" },
    { "seq": 1846, "at": "2026-04-19T23:55:23Z", "kind": "start",    "file": "...", "basename": "2.mp3", "dispatchedTo": "ZIGGY25" },
    { "seq": 1847, "at": "2026-04-19T23:55:26Z", "kind": "failed",   "file": "...", "basename": "3.mp3", "elapsedMs": 2103, "error": "truedat timeout", "dispatchedTo": "ZIGGY25" }
  ]
}
Scan Config
GET /meta/config

Returns current scan configuration keys. All keys are prefixed with scan. for namespace clarity. Does not require MetaServer database to be initialized.

Response: {
  "scan.mode": "auto",
  "scan.includeLocal": true,
  "scan.preferredUrl": null,
  "scan.batchSize": 1,
  "scan.cpuLimit": 25
}
PUT /meta/config

Updates one or more scan config keys and persists to mbxhub-shell.json. Send only the keys you want to change. scan.mode accepts off, local, or auto. Unknown keys are returned in ignored.

Body: { "scan.mode": "local", "scan.batchSize": 4 }
Response: { "status": "ok", "applied": { "scan.mode": "local", "scan.batchSize": 4 } }
GET /meta/config/schema

Returns SchemaEntry-format metadata for all Shell scan settings. Used by the settings UI to build dynamic config panels. Each entry includes key, type, category, tier (0=basic, 1=advanced), description, current, default, and optional min/max/options.

Response: {
  "settings": [
    { "key": "shell.scan.mode", "type": "string", "tier": 0,
       "description": "Scan mode: off disables scanning, local uses this machine, auto discovers peers",
       "current": "auto", "default": "auto", "options": ["off", "local", "auto"] },
    { "key": "shell.scan.includeLocal", "type": "bool", "tier": 0,
       "description": "Include this machine in the auto scanner pool",
       "current": true, "default": true },
    { "key": "shell.scan.preferredUrl", "type": "string", "tier": 0,
       "description": "Preferred remote scanner MetaServer URL (gets priority in auto mode)",
       "current": null, "default": null },
    { "key": "shell.scan.batchSize", "type": "int", "tier": 1,
       "current": 1, "default": 1, "min": 1, "max": 32 },
    { "key": "shell.scan.cpuLimit", "type": "int", "tier": 1,
       "current": 25, "default": 25, "min": 1, "max": 100 }
  ]
}
Feature Lookup
GET /meta/features?path=|audioMd5=|artist=&title=&duration=

Single track mood feature lookup. Parameters: path, audioMd5, or metadata (artist+title+optional album+duration). Server walks the full identity chain in fall-through order: exact-path → path-tail → audioMd5 → metadataKey, regardless of which fields the caller supplied. Each query slot that fails moves on to the next; a single request can resolve a track even when only one identifier is present. Response features block carries the 15 base fields plus the 39 nullable extended-Essentia fields (loudnessMomentary, spectralRolloff, hfc, Bark/ERB/Mel band stats, etc.) — fields the original scan didn't capture stay null. Legacy peer-sync rows stored their metadataKey with duration=0; on metadata miss the server retries the lookup with duration=0, but only accepts the resulting candidate when its stored DurationSeconds is non-zero, which avoids 0.50-confidence false positives against placeholder rows.

Match response (200): {
  "success": true, "matched": true, "confidence": 0.95, "matchType": "audioMd5",
  "features": { "bpm": 128.0, "key": "C", "mode": "major", "danceability": 0.78, ... },
  "identity": { "mediaItemId": "a1b2c3d4-...", "source": "essentia-local", "analyzedAt": "2026-02-15T10:00:00Z" }
}

No match (200): { "success": true, "matched": false, "confidence": 0.0, "matchType": "none" }
POST /meta/features/batch

Batch lookup, max 500 queries per request. Each query uses the same parameter options as the single lookup; the plugin's startup warm-up sends {path, artist, title, album, duration} for every track so the server can fall through to metadataKey for peer-sync rows whose synthetic peer://{bestId} path won't match. Returns results in the same order as queries; each result includes the 39 nullable extended fields when the matched row has them.

Body: {
  "queries": [
    { "path": "C:\\Music\\Artist\\Song.mp3", "artist": "Artist", "title": "Song", "album": "Album", "duration": 245 },
    { "audioMd5": "abc123..." },
    { "artist": "Artist", "title": "Song", "duration": 245 }
  ]
}
Response: { "success": true, "results": [...] }
Over 500: 400 { "error": { "code": "BATCH_TOO_LARGE" } }
POST /meta/identity-coverage

Phase 2 bulk identity coverage probe. For each path, reports which identity signals are present (fingerprint.v1, audioStreamSha256) and whether features exist. Used by the plugin’s /scan/fingerprints sweep to find gaps without issuing N single-path queries. Limit: 500 paths per call.

Body: { "paths": ["C:\\Music\\A\\1.mp3", "C:\\Music\\A\\2.mp3"] }
Response: {
  "success": true,
  "coverage": [
    { "path": "C:\\Music\\A\\1.mp3", "known": true,  "hasFingerprint": true,  "hasStreamSha": false, "hasFeatures": true },
    { "path": "C:\\Music\\A\\2.mp3", "known": false, "hasFingerprint": false, "hasStreamSha": false, "hasFeatures": false }
  ]
}
GET /meta/by-fingerprint/{audioHead64kMd5}

Phase 2 ms-scale probe. Answers do you have this file? using only the cheap fingerprint.v1.audioHead64kMd5 (32-char hex, MD5 of the first 64 KB at TagLib InvariantStartPosition). A hit lets a caller short-circuit the expensive Essentia scan and pull features from this peer. Miss → 404 so the caller can fall through to the next peer or enqueue a local scan. Returns the sibling audioStreamSha256 (if this peer has one) so callers can escalate to the durable byte-identity tier when they want certainty.

Hit (200): { "match": true, "audioStreamSha256": "a4b9…" | null, "hasFeatures": true, "peerId": "PANDABOX" }
Miss (404): { "error": { "code": "NOT_FOUND" } }
Invalid (400): { "error": { "code": "BAD_REQUEST", "message": "audioHead must be 32-char hex" } }
GET /meta/features/by-fingerprint/{audioHead64kMd5}

Phase 2 fingerprint-keyed feature pull. Sibling of /meta/by-fingerprint — after that endpoint confirms a match, this one returns the full feature payload without needing the peer’s local path. matchType = fingerprint.v1, confidence 0.90. Miss → 404.

Hit (200): {
  "success": true, "matched": true, "matchType": "fingerprint.v1", "confidence": 0.90,
  "features": { "bpm": 128.0, "key": "C", ... },
  "identity": { "mediaItemId": "a1b2…", "source": "essentia", "analyzedAt": "2026-04-19T23:55:22Z" },
  "peerId": "PANDABOX"
}
Miss (404): { "error": { "code": "NOT_FOUND" } }
GET /meta/by-hash/{audioStreamSha256}

Phase 2 durable byte-identity feature pull. audioStreamSha256 is the SHA-256 of the metadata-stripped audio region (64-char hex). On hit, returns the full feature payload with matchType="audioStreamSha256" and confidence 0.99 — callers can ingest directly. Miss → 404.

Hit (200): {
  "success": true, "matched": true, "matchType": "audioStreamSha256", "confidence": 0.99,
  "features": { "bpm": 128.0, "key": "C", ... },
  "identity": { "mediaItemId": "a1b2…", "source": "essentia", "analyzedAt": "2026-04-19T23:55:22Z" },
  "peerId": "PANDABOX"
}
Miss (404): { "error": { "code": "NOT_FOUND" } }
GET /meta/fingerprints?since=&limit=

Phase 2 paginated identity.fingerprint.v1.audioHead index dump. Used by joining peers and content-addressed dispatch to learn which tracks this node has cataloged. Filter by MediaItem.ImportedAt via ?since= (ISO 8601). limit default 500, max 5000. Paginate by feeding the returned next back as the new since until done:true. Skips synthetic peer:// paths.

Response: {
  "success": true, "signal": "fingerprint", "count": 500, "done": false, "next": "2026-04-19T23:55:22Z",
  "items": [{ "mediaItemId": "a1b2…", "path": "C:\\Music\\Song.mp3", "value": "abc123…", "importedAt": "2026-04-19T10:00:00Z" }, ...]
}
GET /meta/hashes?since=&limit=

Phase 2 paginated identity.audioStreamSha256 index dump. Same shape as /meta/fingerprints, different key — the durable byte-identity tier.

POST /meta/true-up?limit=N

Phase 2 one-pass convergence sweep. Walks local tracks that have identity.fingerprint.v1 but no MoodFeatures, runs peer-pull (/meta/by-fingerprint/meta/features/by-fingerprint) against known peers, and merges any hits locally. Closes the loop between “identity cataloged” and “features present” without rescanning. Orders of magnitude cheaper than Essentia — safe to run on a schedule. 503 PEER_PULL_UNAVAILABLE if PeerSync is not active.

Query: ?limit=N        // default 500, max 5000
Response: { "success": true, "scanned": 71506, "pulled": 68211, "missed": 3295, "limit": 500 }

// Optional background scheduler in mbxhub-shell.json (off by default):
"meta": {
  "trueUp": {
    "enabled": true,
    "intervalMinutes": 60,   // floor clamped to 5 inside scheduler
    "batchLimit": 500        // clamped to [1, 5000]
  }
}
Ingest & Import
POST /meta/ingest

Scanner posts Essentia analysis results. Walks identity matching (hash → audioMd5 → metadataKey → new), creates or updates MediaItem + MusicDetails + MoodFeatures. Writes are serialized through a single-writer queue. Merge-on-ingest (v0.5.2.4+): when an existing row already has data, base scalar fields are first-write-wins (prevents stale rescans from clobbering good values), but any null extended feature is filled in from the new observation — so a peer rescan with the full 39-field Essentia output enriches earlier 15-field ingests without overwriting them.

Phase 2 hash-only modes: truedat --hash-only --level fingerprint|stream POSTs the same envelope with features omitted and at least one identity signal populated (fingerprint.v1 composite, audioStreamSha256, or both). The ingest is accepted; identity keys are persisted as identity.fingerprint.v1.* (8 sub-keys incl. audioHead, fileSize, pathTail, durationMs, sampleRate, channels, codec, bitrate) and identity.audioStreamSha256. No MoodFeatures row is created until a later scan produces features. This primes ms-scale peer probes (/meta/by-fingerprint) and byte-identity pulls (/meta/by-hash) without paying the Essentia cost up-front.

The features block accepts the 15 original fields (bpm, key, mode, spectralCentroid, spectralFlux, loudness, danceability, onsetRate, zeroCrossingRate, spectralRms, spectralFlatness, dissonance, pitchSalience, chordsChangesRate, mfcc) plus the 39-field extended block (v0.5.2.4+, all nullable): loudnessMomentary, loudnessShortTerm, dynamicRange (LRA in LU from Essentia loudness_ebu128.loudness_range), replayGain, silenceRate20dB, silenceRate30dB, silenceRate60dB, spectralRolloff, spectralComplexity, spectralEntropy, spectralKurtosis, spectralSkewness, spectralSpread, spectralStrongPeak, spectralDecrease, spectralEnergy, spectralEnergyLow, spectralEnergyMidLow, spectralEnergyMidHigh, spectralEnergyHigh, hfc, Bark-band (barkCrest, barkFlatness, barkKurtosis, barkSkewness, barkSpread), ERB-band (erbCrest, erbFlatness, erbKurtosis, erbSkewness, erbSpread), Mel-band (melCrest, melFlatness, melKurtosis, melSkewness, melSpread), beatsLoudness, chordsStrength, hpcpCrest, hpcpEntropy. Scanners should send whatever they computed; missing values stay null.

Body: {
  "path": "\\\\NAS\\Music\\Artist\\Song.mp3",
  "metadata": { "artist": "Artist", "title": "Song", "album": "Album", "duration": 245 },
  "identity": { "audioMd5": "abc123...", "chromaprint": "AQADtN..." },
  "features": { "bpm": 128.0, "key": "C", "mode": "major", "dynamicRange": 11.3, "dynamicRangeSource": "essentia-lra", ... },
  "provenance": { "scannedBy": "pandabox", "tool": "essentia", "toolVersion": "2.1-beta5" }
}
Response: { "success": true, "mediaItemId": "a1b2c3d4-...", "matchType": "new", "confidence": 1.0 }
POST /meta/import

Bootstrap import from mbxmoods.json or mbxhub-fingerprints.json. Batched writes (500 per SaveChanges). Idempotent — re-import updates if newer. Returns immediately with job ID for polling.

Body: { "source": "mbxmoods.json", "path": "C:\\MusicBee\\mbxmoods.json", "libraryRoot": "C:\\MusicBee\\Music" }
Response: { "success": true, "jobId": "import-001" }
GET /meta/import/{jobId}

Poll import progress.

Response: {
  "success": true, "jobId": "import-001", "state": "running",
  "progress": { "processed": 35000, "total": 70000, "errors": 12 },
  "startedAt": "2026-03-16T10:00:00Z"
}
Scan
POST /meta/scan

Enqueue a file for Essentia analysis on this node's local ScanWorker. Used by ScannerPool to dispatch work to remote scanners.

Body: { "file": "C:\\Music\\Artist\\Song.mp3", "share": "\\\\NAS\\Music\\Artist\\Song.mp3", "priority": "high" }
Response: { "status": "queued", "file": "C:\\Music\\Artist\\Song.mp3" }
No worker: 503 { "error": { "code": "SCANNER_UNAVAILABLE" } }
Export & Sync
GET /meta/export

Full export of all tracks with mood features (legacy format, file download). Add ?since={iso8601} for delta export — only tracks with features added or updated since the given timestamp (v1.1 format with identity signals). Scope boundary: this endpoint backs peer-sync, and it filters on MoodFeatures != null. Phase 2/3 identity-only rows (produced by /scan/fingerprints hash-only sweeps — fingerprint.v1 + audioStreamSha256 with no features yet) are not exported here. Identity signals propagate through a different path: TrueUpScheduler + PeerPullService resolve identity-only rows against peer caches on demand (see POST /meta/true-up). Peer-sync remains the mood-data distribution channel.

POST /meta/sync

Trigger immediate peer sync. Runs SSDP discovery and pulls features from all discovered peers.

GET /meta/sync/status

Current sync state: enabled, syncing flag, last result, and known peers with names.

POST /meta/dedup

Collapse peer-sync duplicate rows by grouping on identity.metadataKey, keeping a canonical (prefers non-peer:// path, then latest MoodFeatures.AnalyzedAt), and deleting the rest. Also deletes orphan peer:// rows with no identity signals at all (pure bloat from the pre-B2 era when peer ingests didn't store them). Manual endpoint runs VACUUM to reclaim disk. Auto-dedup (v0.5.2.4+): MetaHost runs AutoDedupIfStale(7d) automatically on every startup right after BackfillMetadataKeySignals; skips VACUUM so startup stays snappy, idempotent via SyncMetadata['dedup.appliedAt'], errors swallowed so a dedup fault can't block MetaServer from coming up.

Response: {
  "success": true,
  "data": {
    "backfilledSignals": 42,
    "mergedDuplicates": 1248,
    "deletedOrphans": 73,
    "remainingTracks": 70885
  }
}
SMTC Target Switching

Runtime management of the Shell's SMTC bridge — discover MBXHub endpoints on the network and switch which one the Shell mirrors. Returns 503 SMTC_UNAVAILABLE if the SMTC bridge is not running.

GET /meta/smtc/target

Current SMTC target (host:port) and connection state.

Response: { "target": "192.168.1.10:8080", "state": "Connected" }
PUT /meta/smtc/target

Switch SMTC target. Tears down existing WebSocket connection, updates config, and reconnects to the new endpoint.

Body: { "target": "192.168.1.10:8080" }
Response: { "ok": true, "target": "192.168.1.10:8080" }
GET /meta/smtc/endpoints

Cached list of discovered MBXHub endpoints on the network. Each entry includes address, name, active flag, and capabilities.

Response: { "endpoints": [
  { "address": "192.168.1.10:8080", "name": "LivingRoom", "active": true, "capabilities": ["rest-api", "websocket"] },
  { "address": "192.168.1.20:8080", "name": "Studio", "active": false, "capabilities": ["rest-api"] }
] }
POST /meta/smtc/endpoints/refresh

Clear endpoint cache and re-scan the network via SSDP for MBXHub instances. Returns the fresh endpoint list.

Identity Matching (5-Tier)

Every lookup walks all tiers before giving up. This enables cross-machine / cross-ingest queries — a file scanned on Machine B under a different drive, user root, or library path is still found. Stored paths are canonicalized to backslash form at ingest; lookups are case-insensitive (SQLite NOCASE collation). The PathTail column holds the last 3 path segments lowercased (e.g. artist\album\song.flac) and is indexed; this is the portable identity key that makes C:\Users\scott\Music\Artist\Album\Song.flac match D:\Library\Artist\Album\Song.flac.

TierSignalConfidenceDescription
1Path (exact)1.0Full path match, case-insensitive, separator-normalized
1bPathTail1.0Last 3 components (artist\album\song.ext), drive- and root-agnostic
2Hash0.95MediaItem.Hash (file content hash)
3audioMd50.95Decoded audio hash (same audio, different container)
4metadataKey0.50Normalized artist|album|title|duration
Shell CLI

Key commands for the MBXHub Shell (MBXHub.exe):

CommandDescription
MBXHub.exe statusShow system health and tool discovery status
MBXHub.exe --no-smtcRun without SMTC bridge (headless/NAS mode)
MBXHub.exe --installRegister AUMID, create Start Menu shortcut, report tool discovery, print Send To setup tip
MBXHub.exe --uninstallRemove AUMID registration and shortcut
MBXHub.exe --reset-dbWipe MetaServer database (re-populates from mbxmoods.json on next start)

RPC Interface

Direct access to all 137 MusicBee API methods. Use this for operations not exposed via REST endpoints or for scripting/automation.

POST /rpc/{methodName}

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"
  }
}
Player Control Methods
MethodParametersReturns
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_SetPositionposition: intboolean
Player_GetVolume-float (0-1)
Player_SetVolumevolume: floatboolean
Player_GetMute-boolean
Player_SetMutemute: booleanboolean
Player_GetShuffle-boolean
Player_SetShuffleshuffle: booleanboolean
Player_GetRepeat-RepeatMode
Player_SetRepeatmode: RepeatModeboolean
Player_GetPlayState-PlayState
Player_GetEqualiserEnabled-boolean
Player_SetEqualiserEnabledenabled: booleanboolean
Player_GetDspEnabled-boolean
Player_SetDspEnabledenabled: booleanboolean
Player_GetCrossfade-boolean
Player_SetCrossfadeenabled: booleanboolean
Player_GetReplayGainMode-ReplayGainMode
Player_SetReplayGainModemode: ReplayGainModeboolean
Player_GetScrobbleEnabled-boolean
Player_SetScrobbleEnabledenabled: booleanboolean
Player_QueueRandomTrackscount: intint
Player_GetOutputDevices-{devices, activeDevice}
Player_SetOutputDevicedeviceName: stringboolean
Now Playing Methods
MethodParametersReturns
NowPlaying_GetFileUrl-string
NowPlaying_GetDuration-int (ms)
NowPlaying_GetFileTagfield: MetaDataTypestring
NowPlaying_GetFileTagsfields: MetaDataType[]string[]
NowPlaying_GetFilePropertytype: FilePropertyTypestring
NowPlaying_GetArtwork-string (base64/path)
NowPlaying_GetArtworkUrl-string
NowPlaying_GetLyrics-string
NowPlaying_GetDownloadedLyrics-string
NowPlaying_GetArtistPicturefadingPercent: intstring
NowPlaying_GetArtistPictureThumb-string
NowPlaying_GetArtistPictureUrlslocalOnly: booleanstring[]
NowPlaying_IsSoundtrack-boolean
NowPlaying_GetSpectrumData-float[]
NowPlaying_GetSoundGraph-float[]
Now Playing List (Queue) Methods
MethodParametersReturns
NowPlayingList_GetCurrentIndex-int
NowPlayingList_GetNextIndexoffset: intint
NowPlayingList_IsAnyPriorTracks-boolean
NowPlayingList_IsAnyFollowingTracks-boolean
NowPlayingList_GetListFileUrlindex: intstring
NowPlayingList_GetFileTagindex: int, field: MetaDataTypestring
NowPlayingList_GetFileTagsindex: int, fields: MetaDataType[]string[]
NowPlayingList_GetFilePropertyindex: int, type: FilePropertyTypestring
NowPlayingList_Clear-boolean
NowPlayingList_PlayNowfileUrl: stringboolean
NowPlayingList_QueueNextfileUrl: stringboolean
NowPlayingList_QueueLastfileUrl: stringboolean
NowPlayingList_QueueFilesNextfileUrls: string[]boolean
NowPlayingList_QueueFilesLastfileUrls: string[]boolean
NowPlayingList_RemoveAtindex: intboolean
NowPlayingList_MoveFilesfromIndices: int[], toIndex: intboolean
NowPlayingList_PlayLibraryShuffled-boolean
NowPlayingList_QueryFilesExquery: stringstring[]
Library Methods
MethodParametersReturns
Library_GetFileTagfileUrl: string, field: MetaDataTypestring
Library_GetFileTagsfileUrl: string, fields: MetaDataType[]string[]
Library_GetFilePropertyfileUrl: string, type: FilePropertyTypestring
Library_SetFileTagfileUrl: string, field: MetaDataType, value: stringboolean
Library_CommitTagsToFilefileUrl: stringboolean
Library_GetLyricsfileUrl: string, type: LyricsTypestring
Library_GetArtworkfileUrl: string, index: intstring
Library_GetArtworkUrlfileUrl: string, index: intstring
Library_GetArtistPictureartistName: string, fadingPercent: intstring
Library_GetArtistPictureThumbartistName: stringstring
Library_GetArtistPictureUrlsartistName: string, localOnly: booleanstring[]
Library_QueryFilesExquery: stringstring[]
Library_QuerySimilarArtistsartistName: string, minimumSimilarity: doublestring
Library_AddFileToLibraryfileUrl: string, category: LibraryCategorystring
Playlist Methods
MethodParametersReturns
Playlist_QueryPlaylists-boolean
Playlist_QueryGetNextPlaylist-string
Playlist_GetNameplaylistUrl: stringstring
Playlist_GetTypeplaylistUrl: stringPlaylistFormat
Playlist_IsInListplaylistUrl: string, filename: stringboolean
Playlist_QueryFilesExplaylistUrl: stringstring[]
Playlist_CreatePlaylistfolderName: string, playlistName: string, filenames: string[]string
Playlist_DeletePlaylistplaylistUrl: stringboolean
Playlist_SetFilesplaylistUrl: string, filenames: string[]boolean
Playlist_AppendFilesplaylistUrl: string, filenames: string[]boolean
Playlist_RemoveAtplaylistUrl: string, index: intboolean
Playlist_MoveFilesplaylistUrl: string, fromIndices: int[], toIndex: intboolean
Playlist_PlayNowplaylistUrl: stringboolean
Podcast Methods
MethodParametersReturns
Podcasts_QuerySubscriptionsquery: stringstring[]
Podcasts_GetSubscriptionid: stringstring[]
Podcasts_GetSubscriptionArtworkid: string, index: intstring (base64)
Podcasts_GetSubscriptionEpisodesid: stringstring[]
Podcasts_GetSubscriptionEpisodeid: string, index: intstring[]
Settings Methods
MethodParametersReturns
Setting_GetPersistentStoragePath-string
Setting_GetSkin-string
Setting_GetSkinElementColourelement: SkinElement, state: ElementState, component: ElementComponentint
Setting_IsWindowBordersSkinned-boolean
Setting_GetFieldNamefield: MetaDataTypestring
Setting_GetDataTypefield: MetaDataTypestring
Setting_GetLastFmUserId-string
Setting_GetWebProxy-string
Setting_GetValuesettingId: SettingIdobject
MusicBee Application Methods
MethodParametersReturns
MB_GetWindowHandle-long
MB_RefreshPanels-true
MB_GetLocalisationid: string, defaultText: stringstring
MB_ShowNowPlayingAssistant-boolean
MB_InvokeCommandcommand: Command, parameter: objectboolean
MB_SetWindowSizewidth: int, height: intboolean
MB_GetVisualiserInformation-{visualiserNames, defaultState, currentState}
MB_ShowVisualiservisualiserName: string, state: WindowStateboolean
Configuration: RPC mode is enabled by default. Disable via settings: "rpcEnabled": false
Access Control: RPC methods respect read-only mode and granular permissions. Write methods (e.g., Player_PlayPause, Library_SetFileTag) require the same permissions as their REST equivalents.

WebSocket Events

Real-time event streaming. Connect once, receive updates automatically - no polling.

URL: ws://localhost:8080/ws
Use REST for: Commands (play, pause), queries (get queue)
Use WebSocket for: Real-time UI updates, visualizations, remote displays
Connection Lifecycle
StepDescription
1. ConnectOpen WebSocket to ws://localhost:8080/ws
2. ReceiveImmediately starts receiving ALL events (default behavior)
3. Subscribe (optional)Send subscribe message to filter to specific events only
4. DisconnectClose 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 Types
EventDescriptionFrequency
TrackChangedNew track started playingOn track change
PlayStateChangedPlay/pause/stop state changedOn state change
VolumeChangedVolume level or mute state changedOn volume change
PositionChangedPlayback position update (milliseconds)~1 per second while playing
QueueChangedNow playing list modified (add/remove/clear)On queue change
ShuffleChangedShuffle mode toggled on/offOn shuffle change
RepeatChangedRepeat mode changed (none/all/one)On repeat change
MetadataChangedRating or love tag changed on current trackOn tag/rating change
ReactionUser reacted to now playing track (emoji, nickname, track info)On reaction submit
TasteChangedAutoQ taste vector updatedOn taste update
ThemeChangedTheme configuration updated (active mode HSL values)On theme change via PUT /system/theme or dashboard toggle
ScanEnqueueTrack needs analysis. Payload: file, priority (high/normal), mode (essentia default, or fingerprint/stream for Phase 2 hash-only sweeps), optional share (UNC path), optional force (true bypasses ScanWorker _pending/_failed gates)On track change (auto-scan), POST /scan/track, or POST /scan/fingerprints
ScanCancelCancel pending scan item. Payload: fileOn track skip before scan completes
MoodCacheWarmedMetaServer mood-cache warm-up completed. Payload: librarySize, fromLocalFile, fromMetaServer, stillMissing, durationMs. Mirrors lastWarmup fields from GET /autoq/mood-cache/status.On startup warm-up and on the 15-minute delta refresh tick
Subscribe/Unsubscribe
// 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", "ScanEnqueue",
              "ScanCancel", "MoodCacheWarmed"]}
Event Data Formats
// TrackChanged
{
  "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"
  }
}

// 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 toggle)
{
  "event": "ThemeChanged",
  "timestamp": "2024-01-03T12:00:06.000Z",
  "data": {
    "activeMode": 1,
    "accentHue": 245,
    "bgHue": 245,
    "bgLightness": 97,
    "textLightness": 15
  }
}

// MoodCacheWarmed (fires when MetaServer mood-cache warm-up finishes)
{
  "event": "MoodCacheWarmed",
  "timestamp": "2024-01-03T12:00:07.000Z",
  "data": {
    "librarySize": 12480,
    "fromLocalFile": 0,
    "fromMetaServer": 9812,
    "stillMissing": 2668,
    "durationMs": 1843
  }
}
JavaScript Example
// 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();
Test Page: Visit /test/websocket to interactively test WebSocket events with subscription controls.

Error Handling

{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description"
  }
}
CodeHTTPDescription
NOT_FOUND404Endpoint or resource not found
INVALID_REQUEST400Invalid parameters or malformed request
ARIA_DISABLED403ARiA input simulation is disabled
FORBIDDEN403Operation not allowed (e.g., RPC disabled)
METHOD_NOT_ALLOWED405Wrong HTTP method
SERVICE_UNAVAILABLE503Required service not available (e.g., TrueShuffle/AutoQ)
INTERNAL_ERROR500Server error

Enumerations Reference

Common enum values used in API parameters and responses.

PlayState
ValueCodeDescription
undefined0Unknown state
loading1Track is loading
playing3Playing
paused6Paused
stopped7Stopped
RepeatMode
ValueCodeDescription
none0No repeat
all1Repeat all tracks
one2Repeat current track
ReplayGainMode
ValueCodeDescription
off0Disabled
track1Track-based gain
album2Album-based gain
smart3Automatic selection
MetaDataType (Common Fields)
FieldCodeDescription
TrackTitle65Track title
Album30Album name
AlbumArtist31Album artist
Artist32Track artist
Composer43Composer
Genre59Genre
Rating75Star rating (0-5)
RatingLove76Love rating
TrackNo86Track number
DiscNo52Disc number
Year88Year
Lyrics114Lyrics text
Comment44Comment
Publisher73Publisher/Label
Conductor45Conductor
FilePropertyType
PropertyCodeDescription
Url2File path/URL
Kind4File type (Music, Video, etc.)
Format5Audio format (MP3, FLAC, etc.)
Size7File size in bytes
Channels8Audio channels
SampleRate9Sample rate (Hz)
Bitrate10Bitrate (kbps)
Duration16Duration (ms)
PlayCount14Play count
SkipCount15Skip count
LastPlayed13Last played date
DateAdded12Date added to library
DateModified11File modification date
PlaylistFormat
FormatCodeDescription
Unknown0Unknown format
M3u1M3U playlist
Xspf2XSPF (XML Shareable Playlist)
Asx3ASX (Windows Media)
Wpl4WPL (Windows Media)
Pls5PLS playlist
Auto7Auto-detect format
LyricsType
TypeCodeDescription
NotSpecified0Any lyrics type
Synchronised1Time-synced lyrics (LRC)
UnSynchronised2Plain text lyrics
LibraryCategory
CategoryCodeDescription
Music0Music files
Audiobook1Audiobooks
Video2Video files
Inbox4Inbox (new files)

Logging

MBXHub includes comprehensive logging for debugging and monitoring. Logs are written using NLog to a log file in the MBXHub folder.

Configuration

Logging is controlled by two settings in mbxhub.json:

SettingTypeDescription
debugModebooleanMaster switch - enables/disables all logging
logLevelstringMinimum log level when debug mode is on
Log Levels
LevelWhat Gets Logged
TraceEverything including request/response bodies, WebSocket message content. Very verbose.
DebugRoute matching, handler selection, subscription changes, internal decisions.
InfoStartup/shutdown, HTTP requests (method/path/status/timing), connections, track changes.
WarningRecoverable errors, timeouts, retries, unexpected but handled situations.
ErrorFailures, exceptions, service unavailable. Always logged even with debug mode off.
Log Location

Log files are stored in the MBXHub subfolder of MusicBee's persistent storage:

%AppData%\MusicBee\MBXHub\mbxhub.log
  • Log files are automatically rotated when they reach 2MB (debug) or 5MB (normal)
  • Old logs are archived as mbxhub.1.log, mbxhub.2.log, etc.
  • Maximum 5 archive files in debug mode, 3 otherwise
Enabling Logging

To enable debug logging:

// In mbxhub.json:
{
  "debugMode": true,
  "logLevel": "Trace"  // or "Debug", "Info", "Warning", "Error"
}
Request Logging Format

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
Performance Note: Trace level logging can impact performance due to the volume of data written. Use Debug or Info level for normal troubleshooting, and only enable Trace when investigating specific issues.

Security

MBXHub operates on a trusted local network model with multiple security layers.

PartyMode Roles

PartyMode uses PIN-based authentication with three roles:

RoleAccessAuthentication
DJFull control: player, queue, start/stop partyDJ PIN via X-Party-PIN header
GuestBrowse library, request songs, voteGuest PIN via X-Party-PIN header
AnonymousRead-only: now playing, artwork, statusNo PIN required
X-Party-PIN Header

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.

Protection Levels

MBXHub supports three protection levels for different deployment scenarios:

LevelDescriptionUse Case
DefaultFull API access, no restrictionsPersonal use, trusted networks
KioskAll requests redirect to defaultPageParty displays, public screens
RestrictedRead-only mode with granular controlsShared access, limited control

Configure via kioskMode and apiReadOnlyMode in settings.

Rate Limiting

PartyMode includes built-in limits and optional per-IP rate limiting:

Built-in limits (always active):

  • Join deduplication: Same nickname can only trigger join announcement once per 60 seconds
  • Request history: Maximum 100 entries kept in memory per session
  • Feed buffer: Maximum 100 items (joins + requests + votes)

Per-IP rate limiting (configurable via Settings → Party Mode...):

  • Requests/min: Max song requests per minute per IP (default: 5)
  • Votes/min: Max votes per minute per IP (default: 5)

Returns 429 Too Many Requests when limits exceeded.

CORS Policy
OriginAccess
localhost, 127.0.0.1Always allowed
192.168.x.xAllowed when allowRemoteConnections is enabled
10.x.x.xAllowed when allowRemoteConnections is enabled
172.16.x.x - 172.31.x.xAllowed when allowRemoteConnections is enabled
External originsBlocked (prevents cross-site attacks)
Request Limits
  • Maximum request body size: 1 MB
  • Content-Type validation for JSON endpoints
  • No stack traces exposed in error responses
Access Control (Read-Only Mode)

MBXHub can restrict write operations via settings with granular per-operation controls:

Restriction Hierarchy:

  • Master read-only - Blocks all write operations API-wide
  • Category restriction - Blocks all operations within a category
  • Operation restriction - Blocks specific operations only

Settings cascade: master OR category OR operation = blocked

Granular Operations:

CategoryOperationEndpoints Affected
LibraryTag editsPUT /library/file/*, POST /library/commit
QueueAdd tracksPOST /queue/add, /queue/playnow, /queue/play
Remove tracksDELETE /queue/*
Reorder tracksPOST /queue/move
PlayerPlaybackPOST /player/play, /pause, /stop, /next, /previous
VolumePOST /player/volume, /mute
SeekPOST /player/position
PlaylistsCreatePOST /playlists
DeleteDELETE /playlists/*
ModifyPUT /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.

SettingDefaultEffect
apiReadOnlyModefalseMaster switch — off
apiReadOnlyPlayerfalsePlayer controls allowed
apiReadOnlyQueuefalseQueue modifications allowed
apiReadOnlyLibraryfalseLibrary allowed (but see granular)
apiReadOnlyPlayliststruePlaylists blocked by default
apiReadOnlyLibraryTagstrueTag edits blocked by default
apiReadOnlyLibraryDeletetrueFile deletion blocked by default
apiReadOnlyPlayerPlaybackfalsePlayback allowed
apiReadOnlyPlayerVolumefalseVolume allowed
apiReadOnlyPlayerSeekfalseSeek allowed
apiReadOnlyQueueAddfalseQueue add allowed
apiReadOnlyQueueRemovefalseQueue remove allowed
apiReadOnlyQueueReorderfalseQueue reorder allowed
apiReadOnlyPlaylistsCreatefalse(moot — parent is true)
apiReadOnlyPlaylistsDeletefalse(moot — parent is true)
apiReadOnlyPlaylistsModifyfalse(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.

Always Permitted (exempt from restrictions):

  • All GET requests - Browse, search, view queue, get status, stream audio/artwork
  • PartyMode guest actions - Song requests, voting, viewing party queue
  • PartyMode DJ role - When party active, DJ bypasses restrictions for player/queue/shuffle

Blocked requests return 403 Forbidden:

{"success":false,"error":{"code":"READ_ONLY","message":"API is in read-only mode"}}
Recommendations:
1. Firewall: Configure Windows Firewall to allow port 8080 only from trusted networks
2. Local Only: Keep allowRemoteConnections disabled unless needed
3. RPC Access: RPC now respects read-only mode and granular permissions. Disable entirely (rpcEnabled: false) if not needed
4. ARiA: Keep ARiA disabled (ariaEnabled: false) unless specifically needed for PC wake scenarios
5. Run Allowlist: The run() command only launches programs defined in ariaAllowedPrograms. Do not add shell interpreters (cmd.exe, powershell.exe) to the allowlist

Integration Notes

File URLs

All file URLs are Windows paths. URL-encode when passing in path parameters:

// Original: C:\Music\Artist\Track.mp3
// Encoded:  C%3A%5CMusic%5CArtist%5CTrack.mp3

// Example: GET /library/file/C%3A%5CMusic%5CArtist%5CTrack.mp3
Pagination

Most list endpoints support offset and limit query parameters:

  • Default limit: 50
  • Maximum limit: 10000
  • Example: GET /library/files?offset=100&limit=50
Sorting

Library endpoints support ?sort= parameter for server-side sorting:

  • alpha (default) - Alphabetical by title+artist
  • artist - By artist name, then title
  • album - By album name, then track number
  • title - By title only
  • date - By date added (newest first)
  • track - By disc number, then track number (natural album order)
  • name - By display name
  • year-asc - By year ascending (chronological, oldest first)

Example: GET /library/files?artist=Pink+Floyd&sort=album

AutoQ Availability

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
Real-time Events

For real-time updates (track changes, state changes), use WebSocket instead of polling:

  • Use REST for: Commands (play, pause), queries (get queue, search)
  • Use WebSocket for: Real-time UI updates, visualizations, progress bars
  • See WebSocket Events section
Network Access

By default, MBXHub only accepts localhost connections. For network access:

  1. Enable allowRemoteConnections in settings
  2. Configure Windows Firewall (use Settings → Firewall → Add Rule)
  3. Clients connect to your PC's IP address (e.g., http://192.168.1.100:8080)
CUE Track Resolution

MBXHub automatically detects CUE-backed audio files and resolves per-track metadata across all surfaces:

  • Player status (/player/status) - Overlays title, artist, trackNo, album from CUE sheet
  • Position (/nowplaying/position) - Includes cueTrack and cueTitle
  • Tags (/nowplaying/tag) - Returns CUE track data for TrackTitle, Artist, Album, TrackNo
  • WebSocket - track events include cueTrack field when CUE active
  • Dashboard - Now-playing shows resolved CUE track metadata

Encoding detection: BOM check → UTF-8 validation → Windows-1252 fallback. Optional CueSharp.dll in Plugins folder enables enhanced parsing via reflection.

REST vs RPC
Use REST when...Use RPC when...
Building a client appWriting automation scripts
Need clean, discoverable URLsNeed direct MusicBee API access
Want resource-oriented designFamiliar with MusicBee plugin API
Working with standard HTTP clientsNeed parameter flexibility