Skip to content

SMP Compatibility Layer ​

The Spider Monkey Panel (SMP) compatibility layer enables running SMP-style scripts within the WebView2 UI environment. The source implementation is in sdk/src/smp/** (bootstrap entry: bootstrapSmpCompat); published dist output is generated and must not be hand-edited.

Quick start ​

html
<!-- 1. The bridge bundle loads first and sets window.fb -->
<script src="bridge.global.js"></script>
<!-- 2. The SMP layer bootstraps on top of window.fb -->
<script src="smp-compat.global.js"></script>

<!-- A module script, so it can await at the top level -->
<script type="module">
  await window.smp.ready;
  console.log('IsPlaying:', fb.IsPlaying);
  console.log('ActivePlaylist:', plman.ActivePlaylist);
</script>

Architecture differences from the lower-level API ​

Architecture Differences

The compatibility layer intentionally preserves SMP synchronous-read semantics (eventual consistency via cached state and events versus asynchronous Promises in fb2k.invoke()). It is not a 1:1 transparent rewrite of every fb2k.invoke() call.

Why a cache exists ​

Most SMP APIs are synchronous. WebView2 ↔ C++ communication is asynchronous (postMessage / invoke). Replacing fb.IsPlaying with await fb2k.invoke('playback.getState') would break existing scripts.

The compatibility layer therefore uses a cache + event model: eventual consistency in exchange for synchronous reads.

Two modes ​

Lower-level API (fb2k.invoke) — each call is an independent request to C++ and returns the live result.

javascript
const state = await fb2k.invoke('playback.getState');

Compatibility layer — reads are local/sync; writes update the cache first and notify C++ asynchronously.

javascript
const playing = fb.IsPlaying; // from _cache.isPlaying
plman.ActivePlaylist = 2;
// immediate: _cache.activePlaylist = 2
// async fire-and-forget: invoke('playlist.setActive', { playlist: 2 })

Benefits ​

  • SMP synchronous-read semantics are preserved for UI-friendly properties
  • High-frequency UI reads do not require per-frame await
  • Fire-and-forget setters make immediate re-reads match SMP expectations
  • Same BridgeCore entrypoint and security checks as the lower-level API

Considerations ​

  1. Transient cache and backend divergence after a failed write: a rejected call (missing method, exception, or timeout) rolls the cache back, but a failure result (such as an out-of-range index) does not and remains until the next event-driven synchronization
  2. Initialization ordering: always await window.smp.ready before reading properties
  3. Non-1:1 behaviors listed in the differences table below

Architecture overview ​

Core entry: sdk/src/smp / published smp-compat.js — cache system, event mapping, plman, and fb extensions.

Wrapper modules under sdk/src/smp/ include:

FileClass / objectRole
utils.jsshared helpershandle normalization, menu item builders
FbMetadbHandle.jsFbMetadbHandletrack handle surface
FbMetadbHandleList.jsFbMetadbHandleListhandle list surface
FbTitleFormat.jsFbTitleFormattitleformat evaluation helpers
FbProfiler.jsFbProfilertiming helper
FbFileInfo.jsFbFileInfometadata/info surface
FbUiSelectionHolder.jsFbUiSelectionHolderselection holder
ContextMenuManager.jsContextMenuManagercontext menu manager
MainMenuManager.jsMainMenuManagermain menu manager

Cache + event mechanism ​

  1. Initialization fills _cache with a batch of backend reads
  2. Runtime listens to bridge events and maps SMP names through fb.onSMP()
  3. Property reads return from cache without another C++ round-trip

Cached fields include playback state, playlist metadata, follow-cursor flags, ReplayGain mode, and common path roots.

fb object — properties ​

PropertyTypeAccessNotes
fb.IsPlayingbooleanread-onlyPlaying state
fb.IsPausedbooleanread-onlyPaused state
fb.Volumenumberread/writeVolume in dB (-100..0)
fb.PlaybackTimenumberread/writePosition in seconds
fb.PlaybackLengthnumberread-onlyCurrent track duration
fb.StopAfterCurrentbooleanread/writeStop after current track
fb.AlwaysOnTopbooleanread/writeAlways-on-top window state
fb.CursorFollowPlaybackbooleanread/writeCursor follows playback
fb.PlaybackFollowCursorbooleanread/writePlayback follows cursor
fb.ReplaygainModenumberread/writeReplayGain mode
fb.ComponentPathstringread-onlyComponent directory
fb.FoobarPathstringread-onlyfoobar2000 install directory
fb.ProfilePathstringread-onlyProfile directory

fb object — methods ​

Playback control ​

fb.Play() / fb.Pause() / fb.Stop() / fb.Next() / fb.Prev() / fb.Random() / fb.PlayOrPause() / fb.VolumeUp() / fb.VolumeDown() / fb.VolumeMute() / fb.Exit()

All return Promises.

Factory methods ​

MethodReturnsNotes
fb.TitleFormat(expr)FbTitleFormatTitleformat object
fb.CreateHandleList()FbMetadbHandleListEmpty handle list
fb.CreateProfiler(name)FbProfilerProfiler
fb.AcquireUiSelectionHolder()FbUiSelectionHolderSelection holder
fb.CreateContextMenuManager()ContextMenuManagerContext menu manager
fb.CreateMainMenuManager()MainMenuManagerMain menu manager

Query methods (Promise) ​

MethodReturnsNotes
fb.GetNowPlaying()FbMetadbHandle | nullNow playing
fb.GetFocusItem()FbMetadbHandle | nullFocus item
fb.GetSelection()FbMetadbHandleListCurrent selection
fb.GetSelectionType()numberSelection type
fb.GetLibraryItems()FbMetadbHandleListFull library
fb.GetQueryItems(handles, query)FbMetadbHandleListLibrary query ⚠️
fb.IsLibraryEnabled()booleanLibrary enabled
fb.IsMetadbInMediaLibrary(handle)booleanMembership check

Command methods (Promise) ​

MethodNotes
fb.RunMainMenuCommand(command)Run main-menu command
fb.RunContextCommand(command)Run context-menu command
fb.ShowConsole()Show console
fb.ShowPreferences()Show preferences
fb.ShowLibrarySearchUI(query)Show library search
fb.ShowPopupMessage(msg, title)Show popup message
fb.Restart()Restart foobar2000

plman object ​

Properties ​

PropertyTypeAccessNotes
plman.ActivePlaylistnumberread/writeActive playlist index
plman.PlayingPlaylistnumberread-onlyPlaying playlist index (-1 when not playing)
plman.PlaylistCountnumberread-onlyPlaylist count
plman.PlaybackOrdernumberread/writePlayback order

Synchronous methods (from cache) ​

MethodReturnsNotes
plman.GetPlaylistName(idx)stringPlaylist name
plman.PlaylistItemCount(idx)numberItem count
plman.FindPlaylist(name)numberFind index (-1 if missing)
plman.IsAutoPlaylist(idx)booleanAutoplaylist flag
plman.IsPlaylistLocked(idx)booleanLock flag
plman.UndoBackup(idx)trueNo-op compatibility shim

Asynchronous methods (Promise) ​

MethodReturnsNotes
plman.CreatePlaylist(pos, name)numberCreate playlist
plman.RemovePlaylist(idx)booleanRemove playlist
plman.RenamePlaylist(idx, name)booleanRename
plman.ClearPlaylist(idx)booleanClear
plman.DuplicatePlaylist(from, name)numberDuplicate
plman.AddLocations(idx, paths, select)numberAdd paths
plman.GetPlaylistItems(idx)FbMetadbHandleListAll items
plman.GetPlaylistSelectedItems(idx)FbMetadbHandleListSelected items
plman.InsertPlaylistItems(pl, base, handles, select)numberInsert handles
plman.GetPlaylistFocusItemIndex(idx)numberFocus index
plman.SetPlaylistFocusItem(idx, item)booleanSet focus
plman.SetPlaylistSelection(idx, items, state)booleanSet selection
plman.ClearPlaylistSelection(idx)booleanClear selection
plman.RemovePlaylistSelection(idx, crop)booleanRemove/crop selection
plman.SortByFormat(idx, pattern, selectedOnly)booleanSort by format
plman.MovePlaylistSelection(idx, delta)booleanMove selection
plman.AddItemToPlaybackQueue(handle)numberQueue append ⚠️
plman.GetPlaybackQueueContents()ArrayQueue contents
plman.CreateAutoPlaylist(idx, name, query, sort, flags)numberCreate autoplaylist
plman.FlushPlaybackQueue()booleanFlush queue

Event system ​

fb.onSMP(eventName, callback) ​

Register a callback using SMP-style event names. Returns an unsubscribe function.

javascript
const unsub = fb.onSMP('on_playback_new_track', (track) => {
  console.log('New track:', track?.title);
});
unsub();

Event mapping ​

Playback

SMP eventWebView2 eventCallback args
on_playback_startingplayback:starting(command, is_paused)
on_playback_new_trackplayback:trackChanged(track_info)
on_playback_stopplayback:stopped(reason: 0=user,1=eof,2=starting_another,3=shutting_down)
on_playback_pauseplayback:paused(is_paused)
on_playback_seekplayback:seeked(time)
on_playback_timeplayback:time(time)
on_playback_order_changedplayback:orderChanged(new_order_index)
on_playback_queue_changedplayback:queueChanged(origin)
on_playback_editedplayback:edited(data)
on_playback_dynamic_infoplayback:dynamicInfo(data)
on_playback_dynamic_info_trackplayback:dynamicInfoTrack(data)
on_item_playedplayback:itemPlayed(handle)
on_volume_changeplayback:volumeChanged(volume_db)

Playlist

SMP eventWebView2 eventCallback args
on_playlist_switchplaylist:activated()
on_playlist_items_addedplaylist:itemsAdded(playlist_idx)
on_playlist_items_removedplaylist:itemsRemoved(playlist_idx, new_count)
on_playlist_items_reorderedplaylist:itemsReordered(playlist_idx)
on_playlist_items_selection_changeplaylist:selectionChanged()
on_item_focus_changeplaylist:focusChanged(playlist, from, to)
on_playlists_changedmulti-event merge()

Selection / metadata / library

SMP eventWebView2 eventCallback args
on_selection_changedselection:changed()
on_metadb_changedmetadb:changed(handle_list, fromHook)
on_library_items_addedlibrary:itemsAdded()
on_library_items_removedlibrary:itemsRemoved()
on_library_items_changedlibrary:itemsModified(undefined, fromHook)

Audio / UI / window

SMP eventWebView2 eventCallback args
on_dsp_preset_changedaudio:dspPresetChanged()
on_output_device_changedaudio:outputDeviceChanged()
on_replaygain_mode_changedaudio:replaygainModeChanged(new_mode)
on_colours_changedui:coloursChanged(data)
on_font_changedui:fontChanged(data)
on_always_on_top_changedwindow:alwaysOnTopChanged(state)
on_cursor_follow_playback_changedplayback:cursorFollowChanged(state)
on_playback_follow_cursor_changedplayback:followCursorChanged(state)
on_playlist_stop_after_current_changedplayback:stopAfterCurrentChanged(state)
on_focuspanel:focus + panel:blur(is_focused)

Utility formatting notes: utils.FormatDuration(seconds) returns H:MM:SS or M:SS.

smp object ​

smp.ready ​

javascript
await window.smp.ready;

smp.refreshCache() ​

Force a full cache refresh after missed events.

javascript
await smp.refreshCache();

smp.dispose() ​

Remove cache-related listeners. Call on unload/navigation.

javascript
smp.dispose();

Wrapper classes ​

FbMetadbHandle ​

javascript
const handle = new FbMetadbHandle('C:\\Music\\song.flac');
handle.Path;       // File path
handle.RawPath;    // Same as Path (for compatibility)
handle.SubSong;    // Subsong index
handle.Length;     // Duration (seconds)
handle.FileSize;   // Size in bytes
handle.HandleId;   // Canonical identifier (including subsong suffix)
handle.Compare(other); // Comparison
await handle.GetFileInfo(); // Returns FbFileInfo

FbMetadbHandleList ​

javascript
const list = fb.CreateHandleList();
list.Add(handle);  list.AddRange(otherList);
list.Count;  list[0];  // Proxy index-based access
list.Find(handle);  list.Clone();
list.Remove(handle);  list.RemoveById(0); // Remove by index
list.RemoveAll();
list.Convert();  // Convert to JavaScript array
list.CalcTotalDuration();  list.CalcTotalSize();

FbTitleFormat ​

javascript
const tf = fb.TitleFormat('%title% - %artist%');
await tf.Eval();                     // Current playing track
await tf.EvalWithMetadb(handle);     // Specific handle
await tf.EvalWithMetadbs(handleList); // Batch evaluation

FbProfiler ​

javascript
const p = fb.CreateProfiler('MyTask');
// ... execute work ...
console.log(p.Time + 'ms');
p.Print();  p.Reset();

FbFileInfo ​

javascript
const info = await handle.GetFileInfo();
info.MetaCount;  info.InfoCount;
info.MetaFind('artist');  info.MetaName(0);
info.MetaValueCount(0);   // Number of values for a specific metadata field
info.MetaValue(0, 0);     // meta[index][value_index]
info.InfoFind('codec');   info.InfoName(0);   info.InfoValue(0);

ContextMenuManager ​

javascript
const cm = fb.CreateContextMenuManager();
cm.InitNowPlaying();  // Or cm.InitContext(handleList) / cm.InitContextPlaylist()
const items = await cm.BuildMenu();
// Display via ui.showCustomMenu or a custom HTML menu
await cm.ExecuteByID(selectedId);
javascript
const mm = fb.CreateMainMenuManager();
mm.Init('file');  // file/edit/view/playback/library/help
const items = await mm.BuildMenu();
await mm.ExecuteByID(selectedId);

Clipboard Operations ​

MethodReturnDescription
fb.CheckClipboardContents()booleanWhether clipboard contains files
fb.GetClipboardContents()FbMetadbHandleListRetrieve file handles from clipboard
fb.CopyHandleListToClipboard(handles)booleanCopy file paths to clipboard
javascript
if (await fb.CheckClipboardContents()) {
    const list = await fb.GetClipboardContents();
    console.log('Clipboard has', list.Count, 'files');
}

await fb.CopyHandleListToClipboard(myHandleList);

DSP and Audio Output ​

MethodReturnDescription
fb.GetDSPPresets()string (JSON)Get available DSP presets
fb.SetDSPPreset(index)voidSet active DSP preset
fb.GetOutputDevices()string (JSON)Get list of audio output devices
fb.SetOutputDevice(guid, deviceId)voidSet active audio output device

GetDSPPresets / GetOutputDevices return JSON strings (matching SMP behavior) and must be parsed with JSON.parse().

javascript
const devices = JSON.parse(await fb.GetOutputDevices());
const wasapi = devices.find(d => d.name.includes('WASAPI'));
if (wasapi) await fb.SetOutputDevice(wasapi.outputId, wasapi.deviceId);

Environment and Commands ​

Method / PropertyTypeDescription
fb.Versionstring (read-only)foobar2000 version string
fb.RunContextCommandWithMetadb(cmd, handle)booleanExecute context command on specific handle; see matching rules below
fb.ClearPlaylist()booleanClear active playlist
fb.GetLibraryRelativePath(handle)stringGet path relative to foobar2000 installation directory

Playlist Queue Operations ​

MethodReturnDescription
plman.AddPlaylistItemToPlaybackQueue(pl, item)booleanAdd playlist item to playback queue
plman.GetPlaybackQueueHandles()FbMetadbHandleListRetrieve all tracks currently in playback queue
plman.IsPlaylistItemSelected(pl, item)booleanCheck whether a playlist item is selected
plman.FindOrCreatePlaylist(name)numberFind existing playlist by name or create a new one
plman.MovePlaylist(from, to)booleanReorder playlist positions
javascript
// Add 3rd track of 1st playlist to playback queue
await plman.AddPlaylistItemToPlaybackQueue(0, 2);

// Find or create playlist
const idx = await plman.FindOrCreatePlaylist('My Favorites');

// Move 4th playlist to 1st position
await plman.MovePlaylist(3, 0);

Known non-1:1 differences ​

APILimitationNotes
fb.GetQueryItems(handles, query)handles ignoredAlways queries the full library
fb.GetFocusItem(force)force=true fallback not implementedReturns null when no focus item exists
plman.FindOrCreatePlaylist(name, unlocked)unlocked ignoredDoes not auto-unlock after create
plman.CreateAutoPlaylist(idx, ...)position arg ignoredAlways appends
plman.AddItemToPlaybackQueue(handle)path-based enqueueEnqueues through queue.addPaths; the path keeps its |subsong:N suffix, so the queued entry is the same subsong
fb.RunContextCommandWithMetadb(cmd, handle)full menu path required, no fallbackThe menu is built for the given tracks; cmd is matched case-insensitively against the full slash-separated path from the menu root, and the first match runs. Returns false when nothing matches or the menu cannot be built or run; it never falls back to the selection or the playing track
window.NotifyOthers(name, info)no automatic on_notify_dataReceivers should use fb2k.on('window:message', ...)
utils.ColourPicker()no native pickerReturns the provided default
All methodsPromise-based where C++ is asyncUnlike original SMP sync APIs
GDI / canvas APIsunsupportedUse HTML/CSS/Canvas instead

Matching Rules for RunContextCommandWithMetadb

The context menu is constructed for the specified tracks. cmd is compared case-insensitively against the full slash-separated path from the root of the menu, and the first matching command is executed. If no command matches, menu creation fails, or execution fails, the method returns false without falling back to current selection or now playing tracks.

window helpers ​

MethodNotesLower-level API
window.GetProperty(name, default?)Persistent property readconfig.get with smp.prop. prefix
window.SetProperty(name, value)Persistent property write (null removes)config.set / config.remove
window.NotifyOthers(name, info)Broadcast to other windowswindow.broadcast

utils object ​

sdk/smp/utils-compat.js provides the SMP utils global object.

Pure JS Methods (no backend dependency) ​

MethodReturnDescription
utils.VersionstringCompatibility layer version
utils.FormatDuration(seconds)stringSeconds → H:MM:SS or M:SS
utils.FormatFileSize(bytes)stringBytes → human-readable size (KB/MB/GB)
utils.SplitFilePath(path)[dir, name, ext]Split file path into directory, file name, and extension
utils.PathWildcardMatch(path, pattern)booleanWildcard pattern matching (*, ?)
utils.DateStringFromTimestamp(ts)stringUnix timestamp → formatted date string
utils.CheckFont(name)booleanCheck whether a font is installed and available

Migration example ​

javascript
await window.smp.ready;

fb.onSMP('on_playback_new_track', async (track) => {
  const tf = fb.TitleFormat('%title% - %artist%');
  const title = await tf.EvalWithMetadb(track);
  console.log('Now playing:', title);
});