Skip to content

SMP compatibility layer

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

Quick start

html
<!-- 1. bridge.js must load first -->
<script src="bridge.js"></script>
<!-- 2. smp-compat.js loads the wrappers -->
<script src="smp-compat.js"></script>

<script>
  await window.smp.ready;
  console.log('IsPlaying:', fb.IsPlaying);
  console.log('ActivePlaylist:', plman.ActivePlaylist);
</script>

Architecture differences from the lower-level API

Read this before porting scripts

The compatibility layer intentionally preserves SMP synchronous-read semantics. 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

Risks

  1. Brief cache/backend divergence after a rejected write until the next event-driven refresh
  2. Initialization ordering: always await window.smp.ready
  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()

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();

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 enqueueSubsong fidelity may be lost
fb.RunContextCommandWithMetadb(cmd, handle)handles ignoredBackend uses default context
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

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

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);
});