Changelog
v2.2.0 (2026-11-11)
New features
Keyboard
- Key presses in the WebView run foobar2000 keyboard shortcuts: while a page has the focus, the commands the user bound under Preferences > Keyboard Shortcuts run. A key without a shortcut reaches the page. In a text box, typing and text editing keys stay with the page; outside a text box, single keys such as Space, letters and the arrow keys can run shortcuts as well. The component's preferences, Window > Keyboard shortcuts, select the keys that take part. By default these are combinations with Ctrl, Alt or Win, Ctrl with an arrow key included, and F1 to F24; single keys need "Other keys". A key held down runs its shortcut once. See Keyboard shortcuts inside the WebView.
keyboard.setReservedKeyskeeps combinations for the page: a listed combination reaches the page even when foobar2000 has a shortcut for it, so a theme's own shortcut takes precedence over the user's binding. The list belongs to the page and is cleared when the page navigates or reloads and when its window closes.allonkeyboard.setReservedKeyskeeps every key for the page, for example while the page records a shortcut in its own editor. System-wide hotkeys and F12 in development server mode still take effect.
Context menu
- Context-menu commands can name the tracks they act on:
discovery.executeContextMenuCommandanddiscovery.executeContextMenuByPathtake atarget:selection: the selection that foobar2000's own context-menu shortcuts use.nowPlaying: only the playing track.playlistSelection: only the selection in the active playlist.auto, the default: the playing track, or the selection in the active playlist when nothing is playing, as before.
Panels
- The Default UI edit-mode placeholder identifies the panel: it shows the panel name, the page source and the options that are turned on, so several WebView panels in one layout can be told apart. It uses Default UI's colours and font, scales with DPI and leaves out less important lines when space is short. Double-clicking the placeholder opens the panel configuration.
- The panel configuration dialog can create a template and open the template folder: New... and Open Folder are next to the template drop-down. A new name follows the rules of the preferences page: letters, numbers, hyphens and underscores only, and no name already in use, ignoring case. The new template is selected and is written to the panel configuration only when OK is clicked.
- The panel configuration dialog has a new layout: the settings form two groups, Page and Behavior, and Edge Style is now under Behavior. "Allow this site to access…" appears only after an external URL is entered; it used to be shown greyed out.
Playlists
playlist.getAutoplaylistQueryreads the query and sort of built-in autoplaylists: for a list driven by foobar2000's own query/sort client, such as one created in Default UI or withplaylist.createAutoplaylist,queryis the current query and the newsortis the sort expression, an empty string when none is set. Both are read on every call. For lists of other providers and lists managed by this component,queryandsortarenull, andnotegives the reason. Earlier versions returnquery: nullfor every list and nosortfield.
Changes
keyboard.registerHotkeyrefuses combinations that would take common keys from every program: a combination must contain Ctrl, Alt or Win; a media key can be registered alone. Ctrl+A, C, V, X, Y and Z, Ctrl+Shift+Z, Ctrl+Esc, Alt+F4, Alt+Tab and Alt+Space are refused as well. A refused combination fails withINVALID_PARAMS. These combinations used to register, but no hotkey ever fired (see Fixed).- A taken combination is reported with its holder: when this component has already registered the combination,
keyboard.registerHotkeyfails withOPERATION_FAILED, anddetailsgives theidandwindowIdof that registration. When another program or a global foobar2000 shortcut holds it, the call fails withOPERATION_FAILED, anddetails.win32Errorholds the Windows error code. - Hotkeys are released when the window that registered them closes: a hotkey used to stay registered after its window closed, and its presses went to the main window's page. A Default UI or Columns UI layout has no main window page, so a closed panel kept the combination and a recreated panel could not register it again.
- Hotkey entries of
keyboard.getRegisteredHotkeyshave a newwindowId: the window that registered the hotkey and receives its presses. - Ctrl+P, Ctrl+O and Ctrl+N follow the user's foobar2000 bindings: while a page had the focus, these combinations always opened Preferences, opened files and created a playlist, whatever the user had bound. They now run the shortcuts set under Preferences > Keyboard Shortcuts, like any other key. A combination without a binding reaches the page.
- F5 reloads a page that has the focus unless the page handles the key: F5 used to reach the page as an ordinary key press, and the page reloaded only when the focus was outside it, or through the browser when developer tools were enabled. The page now receives the key press first. When no
keydownlistener callspreventDefault(), the component reloads the page, in every window type. While the page is loading or after it failed to load, F5 reloads at once. See Keyboard shortcuts inside the WebView. keyboard.registerShortcutis deprecated and will be removed in 3.0: it stores the combination, but no key press is matched against it. For shortcuts inside the page, listen forkeydown.fb.GetLibraryItemsin the SMP compatibility layer is faster: it pages throughlibrary.searchand reads only the fields that handles use. On a library of about 100,000 tracks, the call took about 0.8 seconds instead of 4.2 in testing. It returns the same tracks in the same order.
Fixed
Keyboard
- Hotkeys registered with
keyboard.registerHotkeyfire: they were registered on the foobar2000 main window, whoseWM_HOTKEYmessages the component does not receive, so registration succeeded butkeyboard:hotkeywas never sent. Hotkeys are now registered on a message window of the component, and a press goes to the window that registered the hotkey. A page's hotkeys are released when the page navigates or reloads, so a reloaded page can register the same combination again. All hotkeys are released when foobar2000 exits.
Tag Writing
- WAV tag writes no longer destroy non-ASCII tags:
metadata.write,metadata.writeBatch,metadata.removeTag,metadata.removeField, the tag fallback ofrating.set, the embedded target oflyrics.saveandreplaygain.clearrefuse to write a WAV file whose tags would then contain non-ASCII text. They fail withNOT_SUPPORTED,details.reasoniswavInfoNonAscii, and the file is unchanged. foobar2000 stores WAV tags in the RIFF INFO chunk as ASCII and writes every other character as?. It also reads a GBK-encoded INFO chunk as Latin-1, so rewriting such a file, even to change another field, replaces its original tags with?for good. WAV files whose tags are all ASCII are written as before, and other formats are not affected.
Media Reading and Artwork
- Media stream and cover URLs work in pages loaded through a folder mapping: in pages loaded through a folder mapping such as
foo-ui-webview2.local, which is how packaged themes load, the media stream and cover URLs issued by the host always failed withERR_FILE_NOT_FOUND. Both kinds of URL now use the hostfb2k-routes.invalid, which no folder mapping covers. These URLs are cross-origin for every page, so a page that reads cover pixels from a canvas must setcrossoriginon the<img>. - A stuck cloud or network read no longer blocks local reads: cover and lyrics reads shared one queue, so a read of a cloud placeholder file or an unreachable network share that hung made every later local read time out until foobar2000 was restarted. Files on network shares and mapped drives, and files that are only in the cloud, are now read on a separate queue.
- Reading the cover of a cloud-only file no longer downloads the file: for a file on a local drive that is only in the cloud,
artwork.getCurrent,getByPath,getForTrack,getByPlaylistItem,getBatch,getAvailableTypes,getAvailableArtworkandgetMetadatareport no cover without opening the file. They used to download the whole file. Thanks to @Sijie20 for reporting.
Playlists
playlist.replaceAllAndPlayplays the track thatplayIndexnames: the rows it wrote were deduplicated and sorted by the user's order for incoming items, whileplayIndexcounted the sorted rows, so another track played whenever that order differed frompaths. The rows now follow the order ofpaths; when every path is one track,playIndexis the index of the path to play. Duplicate paths are kept, and the tracks of a folder are added in the order foobar2000 expands the folder. Thanks to @LYY-LZH for reporting.
Panels
- Right-clicking a panel in Default UI edit mode opens Default UI's standard edit menu: Replace, Cut, Copy and Paste are available, and Configure... is in the same menu. Only Configure... used to appear.
- A panel created while edit mode is on shows its placeholder at once: it used to show the page at full size, so the page received the right-click and the panel could not be configured.
- Transparent areas of a panel follow foobar2000's colours: where the page draws no background, the panel shows the host's current background colour: Default UI's background colour in a Default UI panel, and the Columns UI global background colour in a Columns UI panel. When the colours are changed in Preferences, these areas change at once. A Default UI panel used to keep the colour it had when it was created, and a Columns UI panel was always white.
- The panel setting "Transparent Background" takes effect when the panel is created: a panel saved as opaque stays opaque after foobar2000 restarts or the panel is rebuilt; the setting used to apply only when it was toggled. The opaque background is now the host's background colour instead of a fixed dark grey (
#1e1e1e). - The name dialog for creating and renaming templates follows foobar2000's light and dark mode: it was always light.
- The starter page of a new template uses neutral grey text: the text was white and could not be read where the transparent part of a panel shows a light background. Existing templates are not changed.
v2.1.0 (2026-10-27)
New features
Playback and JIT Session experimental
- Document-bound playback (
jitSession): keeps playback attempts and operation results across page reloads, binds control to the calling document, and supports explicit handover between windows. A next entry can be withdrawn without stopping the current track; near the end of a track when foobar2000 may have already picked the next entry, withdrawal is rejected withnextPreselected. When the next entry is not ready,skiprejects the request without affecting the native playback queue. When a new resource forjitSession.playis still being resolved, the new attempt remains pending even if the previous track ends. When native playback advances to the buffered next entry, unfinished attempts and operations from the previous entry are marked assupersededrather than reported as EOF.jitSession.releaseresolves only after the buffer has cleared; if clearing fails, retrying with the samerequestIdreturns the same error. ExistingjitQueuemethods retain their behavior. - Client state synchronization (
JitSessionClient): manages credentials, serializes commands, reconciles missed events, and awaits operation results. Missed terminal events are queried automatically during waiting; if further event gaps occur during reconciliation, querying continues until states are aligned. - Native playback adoption (
prepareAdoption): snapshots the active track, native playback queue entries, and the source playlist without mutating them;jitSession.getNativeContextpaginates the snapshot, andjitSession.adoptNativeadopts the active track without playback interruption. Stopping or releasing the session restores remaining queue items to native playback.jitSession.playNativeandjitSession.setNativeNextplay or buffer source rows or queue items from the adopted context, whilejitSession.setNativeQueueremoves or reorders pending return items. When the source playlist has no remaining items, the final queue item is returned through a host-retained single-track playlist (__webview_restore__) to stop after playback completes. CallingjitQueuemethods during adoption continues as normal, leaving the sessionstoppedand only permitting release of the adopted context.getCapabilities()reportsadoption: true. - Failure recheck diagnostics (
attemptRechecked): when a playback attempt ends asunknownwithin its first second, the host reopens the resource and reports whether it failed again along with specific reasons (accessDenied,notFound,unsupportedContent,network), as well as whether foobar2000 recorded a playback failure.getCapabilities().failureRecheckistrue;preciseFailureremainsfalse. - Background execution: while a
jitSessionis active, minimizing the window, hiding it to the notification area (tray), or locking the screen no longer freezes the controlling page: the page continues responding tojitSession:needNext, preventing playback stalls past buffered entries. The page remains hidden with lowered memory allocation; deep suspend resumes once the session stops or is released.getCapabilities().backgroundExecutionistrue.
Media Library and Incremental Sync
- Incremental library sync (
library.getChanges/library.getTracks): enable pages to maintain local library caches with incremental synchronization.library.getChangesreturns persistent revisions surviving restarts alongside tracks added, modified, or removed since a given revision; offline changes made while foobar2000 was closed arrive once startup indexing finishes.library.getTracksfetches track rows for up to 5,000 handles per call. See Keeping a local copy. - Granular change notifications and batch handles:
library:itemsAdded,library:itemsRemoved,library:itemsModified, andmetadb:changedaddhandlesandtruncated.handleslists each modified track'shandle; when more than 5,000 tracks change simultaneously,truncatedistrueandhandlesis empty. Passhandlestotitleformat.evalFieldsBatchto read only changed rows. Tag edits no longer rebuild directory indexes used bylibrary.getRootsandlibrary.browseTree.fb-playlist-viewimmediately re-reads modified track rows without waiting for scroll events, and the SMP compatibility layer'son_metadb_changedaccepts up to 5,000 modified tracks at once (previously 50). - Third-party hook refresh bypass (
library:itemsModified): addsfromHook. Set totruewhen a component such as foo_playcount requests foobar2000 to refresh external fields rather than actual tag edits. On such changes, the host discards only the cachedlibrary.getAlllist while retaining other library caches, so playback-driven play count and rating writes no longer rebuildlibrary.getRootsorlibrary.getAlbums. SDK library tree components do not reload on this event.
Playlists and Queue
- Version tracking and conflict protection (
contentVersion):playlist.getTracks,getTracksAt,getSelectedTracks,getSelection,getMatchingRows,getGroupRuns, andqueue.getreturncontentVersion. It increments whenever rows are inserted, deleted, reordered, or replaced—even if total count is unchanged—preventing pagination mismatches across differing versions.playlist.insertTracks,removeTracks,moveTracks,reorder,setSelection, andqueue.remove,moveToTop,setContents,insertNext,playNowacceptexpectedVersion: if target rows changed since that version, the call fails withSTALE_TARGETwithout making mutations, and returns the updated version on success.playlist:itemsAdded,itemsRemoved,itemsReordered,itemsReplaced, andplayback:queueChangedevents carry the updated version.playlist.playTrackandsetFocusedTrackalso acceptexpectedVersion.queue.add, plus playlist row items passed toqueue.setContentsorqueue.insertNext, acceptexpectedPlaylistVersionfor the source playlist'scontentVersion. SDK methods placeexpectedVersionas their final optional parameter. - Managed autoplaylists (
playlist.createManagedAutoplaylist): creates managed autoplaylists whose query strings, sort rules, and keep-sorted settings can be read viaplaylist.getAutoplaylistConfigand updated viaplaylist.setAutoplaylistConfig. Query execution remains native within foobar2000, but managed playlists require foo_ui_webview2 to remain installed. Updates must include the baserevision; conflicting revisions fail withdetails.reason: 'revisionConflict'. Config updates broadcastplaylist:autoplaylistConfigChanged. Other autoplaylists (including those created byplaylist.createAutoplaylist) do not support configuration reads or updates. - Native autoplaylist editor invocation (
playlist.showAutoplaylistUI): opens the native editor for a specified autoplaylist by index orplaylistGuidwithout activating the playlist or starting playback. The request returns immediately once accepted; native editor execution status is communicated viaplaylist:autoplaylistUIResult(reflecting invocation success rather than user save/cancel actions).playlist.getAutoplaylistInfoaddseditorAvailabilityandeditorAvailabilityReasonto check editor availability without opening windows. The SDK addsplaylist.showAutoplaylistUIandplaylist.onAutoplaylistUIResult. - Path expansion status tracking (
playlist:addComplete): carriesstatus:completed;partialorfailedwithreason(targetRemoved,locked) if the target playlist is removed or locked before expanded items are inserted; orcancelledwhen user cancels expansion.
Context Menu and Actions
- Pre-execution menu validation (
menu.runContextCommandById): supports pre-execution menu state validation: pass the node'spathasexpectPathand thetargetKeyreported bymenu.getContextMenuasexpectTarget. If tracks, the command ID, or command availability change after opening the menu, execution fails withdetails.reasonwithout performing any action, preventing unintended execution. Calls omitting these parameters retain their previous behavior. The SMP layer'sRunContextCommandWithMetadbandContextMenuManager.ExecuteByIDnow use this check. - Native command completion feedback:
menu.runMainMenuCommand,menu.runContextCommand, andmenu.runContextCommandByIdresponses includecompletion: 'unknown': the command is handed over to foobar2000 for native execution; the host does not track whether execution has finished or whether opened dialogs were cancelled.
Window and Bridge Infrastructure
- Scale factors and coordinate conversion:
window.getZoomreturnstextScaleFactor, andwindow.setDragRegions,window.setNoDragRegions, andwindow.setClickThroughExcludeRegionsreturnscale(the scale factor applied to rectangle coordinates). - Invocation timeout protection: calls without responses within 30 seconds reject with
code: 'TIMEOUT'naming the method in the error message. Note that the operation may have already completed in the host; see Operation results. - Native tray click action (
tray.setClickAction): withtoggleMainWindow, a left click on the tray icon hides or shows the main window natively, without running page code, so it responds even while the page is suspended; a double-click toggles once. The defaulteventkeeps sendingtray:clickandtray:doubleClick. The action can be set beforetray.createand is reset toeventbytray.destroy; in panels the call fails withPANEL_MODE_UNSUPPORTED. Remove window commands from existing click handlers before switching.
Changes
- DnD capability reason enum cleanup:
dnd.getCapabilitiesanddnd:capabilitiesChangedremovedisplacedfrompathsUnavailableReasontype definitions (the host never reported this value). Remove comparisons against'displaced'in TypeScript code. - Standardized playlist column IDs:
fb-playlist-viewno longer accepts localized Chinese column display names (播放次数, 首次播放, 最近播放, 添加日期, 等级) as column IDs. Use canonical IDs (play_count,first_played,last_played,added,rating), or supply the columnpatternfromfb.playlist.getAvailableColumns()informats; see Playlist components. - Non-local panel URL permission gate: non-local panel URL overrides (not on
localhost,127.x.x.x, or[::1]) require enabling Allow this site to access foobar2000 and this computer in panel configuration before API calls are permitted; until enabled, pages render normally but API calls fail withORIGIN_DENIED. URLs configured before upgrading retain permission. Changing URLs or disabling the setting takes effect immediately upon confirming the dialog without restarting foobar2000. - Main menu commands addressed by full identity (
menu.runMainMenuCommand): a slash-separated path must match every ancestor; a wrong prefix no longer falls back to another command with the same leaf name. A dynamic leaf addressed by GUID needs bothguidandsubGuid, and a dynamic container cannot be run. The response carriesguidalso when a name or path was resolved. In menu trees,executableon a main-menu item means its address is unique and complete, whether or not the item is enabled;unaddressableReasonaddsambiguousMatch,missingDynamicIdentifier,dynamicParentandincompleteHierarchy, and an ambiguous item listsaddressCandidatesfor diagnosis only.discovery.executeMainMenuCommandfails withMENU_MATCH_AMBIGUOUSorMENU_ITEM_DISABLEDfor ambiguous or disabled commands. When the menu state cannot be read,menu.getMainMenu,menu.runMainMenuCommandanddiscovery.executeMainMenuCommandfail withOPERATION_FAILEDinstead of treating the item as enabled. window.focusin panels: called from a DUI/CUI panel withoutwindowId,window.focusbrings the foobar2000 main window to the foreground instead of failing withPANEL_MODE_UNSUPPORTED. Success means the request was made; Windows may still refuse the foreground.
Fixed
Window Scaling and High-DPI Adaptation
- Text scaling title bar hit-testing and DPR alignment: fixed scaling and title bar hit-testing in standalone windows when Windows text scaling exceeds 100% (Settings > Accessibility > Text size): custom buttons in title bars are no longer misidentified as drag regions,
devicePixelRatioaligns with actual page scale, andapp-regionareas align with the page. Dynamic text size adjustments while running are supported. - Custom drag region scale conversion: fixed
window.setDragRegions,window.setNoDragRegions, andwindow.setClickThroughExcludeRegionscoordinate calculations to convert using pagedevicePixelRatio(combining page zoom and Windows text scaling). Page code manually multiplying rectangles by zoom should remove that multiplication.window.setMaximizeButtonRegionalso incorporates text scaling. - Popup URL query parameter concatenation: fixed URL query string corruption in
window.createPopup..htmlpages containing existing queries (page.html?mode=1) or hash fragments properly append&windowId=...instead of a second?. - Window protection without active tray icon: fixed window disappearance when "Minimize to tray" or "Close to tray" is enabled without an active tray icon. If
tray.createwas not called, minimize and close actions fall back to taskbar minimization. Callingtray.destroyalso restores main windows currently hidden to the tray. - System drive media permission relaxation: updated permission checks for
shell.showInExplorer,shell.openWith,clipboard.writeFiles, andartwork.getFolderImagestoMediaRead(previouslyRead), allowing media library and playlist files located on system drives to be revealed in Explorer, opened with default apps, or copied to clipboard. - Windows 10 backdrop and corner fallback: on Windows 10, requests for Mica, Mica Alt or Acrylic fall back to a solid background, and the host no longer asks for rounded corners. The window capabilities
supportsCornerPreferenceandsupportsMicaAltreportfalsethere. Windows 11 is unchanged. - Popup edge resizing: popups with
behavior.noActivate, or ones that have entered click-through mode, resize by their edges without entering Windows' modal sizing loop, so the page keeps rendering during the drag. For other popups, rapid size updates during a drag are merged and the final size is always applied.
Runtime and Diagnostics Stability
- WebView2 creation failure native error dialog: fixed silent startup failure when WebView2 cannot be created in standalone mode (e.g. missing runtime). Displays a native error dialog explaining the issue with options to copy diagnostic info, open preferences, or open the console.
- Runtime unavailable native fallback menu: fixed missing fallback UI when WebView2 is unavailable. Right-clicking the window client area displays a native context menu providing Preferences, Console, Reload, Error details, and Exit.
- Lifecycle diagnostic logging: improved
webview_lifecycle.login the profile folder to record component and foobar2000 versions, Windows build, WebView2 runtime version, active data folder, DPI awareness, compatibility flags, WebView2 environment variables, and elevation status at startup and on failure, including HRESULT error codes. - Custom menu crash protection: fixed crash when calling
ui.showCustomMenu. Thanks to @MoveToEx for reporting. - Slow storage cover wall responsiveness: fixed main window unresponsiveness caused by cover wall loading on slow or cloud-backed storage. Thanks to @Sijie20 for reporting.
- Offline network share timeout non-blocking: fixed main thread blocking during network timeouts in
file.read,file.exists,file.list, andfile.getInfofor disconnected network shares or mapped drives; slow-call console logs now report specific file paths. - Lyrics reads off the main thread:
lyrics.getandlyrics.existsread tags and lyrics files on a background thread, so media on slow or cloud storage no longer freezes the window. - No probing of unrelated network drives: checking a local media path no longer opens every mapped network share, so a disconnected mapped drive no longer delays reads of local files.
JIT Queue Mechanisms
- Shadow playlist row attribution: fixed
jitQueuetrack attribution to identify entries by shadow playlist row rather than raw path, preventing session termination on plain paths, duplicate buffering consumption on replay, and properly terminating sessions when playing different files. - Subsong path playback accuracy: fixed
jitQueue.playNowandjitQueue.enqueueNexthandling of subsong paths (path|subsong:N): local paths no longer play subsong 0 by default, and streaming URLs containing query strings play correctly. - Resolution race conditions and supersession: fixed race conditions when resolving
jitQueuetracks: superseded tracks (from newerplayNow,enqueueNext,stop,clear,preloadBatch, or manual track changes) are discarded without triggering error events, and replaced next tracks do not reinsert stale resources into buffers. - Consecutive call conflict resolution: fixed consecutive
jitQueue.playNowcalls failing withOPERATION_FAILED: newer requests supersede in-flight resolutions cleanly. - Native playback queue preservation: fixed
jitQueue.playNowandjitQueue.skippreserving native playback queues: queued tracks remain intact during JIT playback, playing sequentially after JIT items finish. When no buffered items remain,skipexecutes foobar2000 native Next. - Late resolution result protection: fixed late
jitQueueresolution results interrupting newer tracks after native playback advances; replaying current tracks preserves pending next track resolutions.
Media Library and Playlists
- Startup initialization field correction: fixed
library.getStatusinitializedfield to accurately indicate whether startup library indexing has completed (previously duplicatedenabled). - Malformed UTF-8 tag tolerance: fixed malformed UTF-8 handling in tags: album lists, metadata reads, and lyrics return successfully with invalid characters safely replaced (U+FFFD) for display without modifying underlying files.
- Damaged duplicate album identity preservation: fixed album deduplication for damaged names: albums displaying identical damaged names maintain distinct identities; SDK and library tree preserve identities and embedded CUE subsongs during loading. Such album rows carry
albumId; pass it tolibrary.getAlbumTracksto read that album's tracks. - Malformed tag serialization tolerance: fixed metadata serialization failure on malformed tags: invalid tag names are omitted without overwriting valid fields, and malformed tag values or codec text no longer interrupt asynchronous metadata serialization.
- Expansion cancellation event completion: fixed
playlist.addPathsAsyncomitting completion events when cancelling progress dialogs: cancellations now reliably emitplaylist:addCompletewithstatus: 'cancelled'. - Tracks inside archives: methods whose paths are checked as
MediaReadaccept tracks inside archives (unpack://…) and portable-relative paths (file-relative://…); the host checks the archive's actual location. Previously these calls failed withPERMISSION_DENIED. Nested archives and archive paths with non-ASCII names are also parsed correctly. - Portable profile under system folders: when portable foobar2000 is installed under
C:\Program Files,C:\Program Files (x86)orC:\ProgramData, pages can read and write its profile folder; the install folder itself stays blocked. The Pictures folder joins the folders readable on the system drive. - Full-track waveform decoded format:
audio.generateFullWaveformandaudio:fullWaveformReadysize the waveform windows by the PCM format the decoder actually outputs, andsampleRateandchannelsreport that format, which can differ from the file's nominal one. Previously the windows followed the nominal sample rate, so the waveform did not line up with the track when the two differed.
Drag and Drop Interaction
- Internal drag-and-drop reordering: fixed internal drag-and-drop reordering in standalone windows: page-initiated drags with
dropEffect: 'move'drop correctly, allowing row and tab reordering without forbidden cursor states or lostdropevents. External drag-ins continue as copy operations; panel embedded modes remain unchanged. - Unhandled element drop rejection: fixed unintended file drop acceptance on unhandled text boxes and
contenteditableelements: unhandled elements reject file drops with forbidden cursors and emit nodnd:dropevents. File input controls and text selection drag-and-drop remain functional.
v2.0.0 (2026-10-06)
Starting with this release, foo_ui_webview2 follows semantic versioning: the 2.x series will contain no further breaking changes. See Versioning and compatibility.
Breaking changes in this release
- Calls with an undeclared parameter key (other than one starting with
_), a value of the wrong type or a missing required value fail withINVALID_PARAMS, anderrornames the parameter. Previously, extra keys were ignored. - Only pages the host trusts can call the API. Calls from a popup at an
http(s)URL its opener does not trust, a page that navigated away from a trusted origin, afile://page or, in development mode, a page not on the configured server fail at once withORIGIN_DENIED, and such pages receive no events. See Which pages can call the API. - SDK list methods now resolve with the host's envelope and no longer throw on failure:
playlist.getAll,getTracks,getSelectedTracksandgetAvailableColumns,output.getDevices, thesystemandconfiglists, andplaycount.get. Read lists from the corresponding field, or useunwrap. - Generated SDK
XxxResponsetypes are nowXxxSuccess | ApiFailure; checksuccessbefore reading fields. Several event payload types are narrower,artwork.getAvailableArtworkrequirespath,metadata.embedArtworkrequiresopts, andFbBaseElement._subaccepts only names listed inFBEventName. These affect TypeScript builds only. - Without a host, SDK calls resolve with
{ success: false, code: 'NOT_SUPPORTED', error, details: { method } }instead of{ mock: true, method }. To run a page against fake data, assign a stand-in towindow.fb2k. - Track events carry the shared
Trackand no longer includeidorfullPath; readhandleinstead. This affectsplayback:trackChanged,playback:edited,playback:itemPlayed, and thetrackandnowPlayingofselection:changed. library.getAlbumTracksnow takes thename(asalbum) andalbumArtistof alibrary.getAlbumsrow; both are required, andartistis no longer accepted. The result reportsalbumArtistinstead ofartist.- Some calls now fail differently:
- On a locked playlist,
library.addToPlaylist,playback.playPathandplayback.playPathsfail withLOCKED(previously they reported success), andqueue.addPathsfails withLOCKED(previouslyOPERATION_FAILED), withplaylistandisLockedmoved intodetails. - Without an active playlist,
queue.addfails withNO_ACTIVE_ITEM(previouslyINVALID_INDEX). artwork.getByPlaylistItemwith a playlist index past the last playlist fails withINVALID_INDEX(previouslyNOT_FOUND).- Negative rows and queue positions fail with
INVALID_PARAMS(previously they were dropped, or failed withINVALID_INDEXinplaylist.reorderandreorderPlaylists). - A context menu whose
handlesare all unusable fails withINVALID_PARAMSinstead of acting on the playing track, the selection or the playlist.
- On a locked playlist,
- File drops are accepted only where the page calls
preventDefault()in itsdragoverhandler; elsewhere the cursor shows the forbidden sign and nodnd:dropis sent. lyrics.saveno longer acceptsconfig, andallwrites onlyfileandembedded. Subsongs no longer read numbered or shared album lyrics files automatically; specify the file withfilename. Existing lyrics are not deleted; see the migration notes.
New features
- Playlists can be specified by GUID.
playlist.getAll,getActive,getPlaying,create,duplicateandcreateAutoplaylistreportguid, and every method that acts on one playlist, includinglibrary.addToPlaylist,queue.add,queue.addPathsandartwork.getByPlaylistItem, acceptsplaylistGuid. A playlist that has been removed fails withNOT_FOUND. - Responses that report a playlist by index also include its
playlistGuid, and theplaylist:*events carry GUIDs (playlistGuid,guid,newGuid,guids). playlist.setActiveandplaylist.renameacceptplaylistGuid,playlist.reorderPlaylistsacceptsnewOrderGuids, and the entries ofqueue.setContentsandqueue.insertNextacceptplaylistGuid.- New
playlist.getMatchingRows(rows that match a query) andplaylist.getTracksAt(tracks by row number). - SDK: methods that take a playlist accept a
PlaylistRef(index or GUID);<fb-playlist-view>and<fb-playlist-tabs>identify playlists by GUID. - Position readings include
hostTime(Unix milliseconds, on the same clock asDate.now()) inplayback.getPosition,playback.setPosition,playback:seeked,playback:timeHighResandplayback:stateChanged. The SDK addsPlaybackClockto estimate the position between updates. playback:stateChangedincludescanSeek.- Local media (experimental; may change in later releases): new
media.getStreamUrlandmedia.getContainerInfo; the SDK addsMediaElementFollowerandcanPlay. See Media API and Local media. - PCM (experimental; may change in later releases): new
audio.decodePcmdecodes a track, or a range of it, into float32 PCM, and newaudio.subscribeStreamsubscribes to the audio foobar2000 is playing. Samples arrive through shared buffers, which the SDK reads withPcmBufferandPcmStream. audio.subscribeSpectrumacceptsoutput: 'bins'to deliver raw FFT bins, split intoleftandrightwithchannels: 'stereo'. The SDK callback receivesSpectrumBinsFrame.metadata.writeandmetadata.writeBatchwrite multivalue tags from a string array; an empty array removes the tag.- Track rows include
albumArtists, which thefieldsoflibrary.query,library.searchandplaylist.getTrackscan select. lyrics.getandlyrics.existsacceptfilenameto specify the lyrics file.- New
webview.getSourcereports where the page was loaded from. - New
window.setMaximizeButtonRegiongives a page-drawn maximize button Snap layouts on Windows 11. window:stateChangedincludeswindowId.file.write,fb.file.writeBinaryandfb.file.writeDataUrlacceptatomic: true, which cannot be combined withappend.- New
tray.setMenuZonesreplaces the whole tray menu in one call. dnd:enter,dnd:dropanddnd.getPathsAsyncincludesource, which distinguishes drags from the page itself ('self'), from another window of the same foobar2000 ('other-window') and from elsewhere ('external').- Portable installations now keep their WebView2 data under their own profile folder, so multiple instances can run at the same time. The first start copies the web data from the shared folder; if another foobar2000 is still using that folder, the instance starts with empty web storage and the console shows both folders. Settings saved with
config.setare not affected. - The documentation site splits the API reference into one page per namespace and adds an SDK type reference, a getting-started tutorial and an example of computing a spectrum from the live PCM stream.
Changes
library.getAlbumsandlibrary.getAlbumTracksshare one grouping of the library, so opening the same album again no longer scans the library.library.getAlbumTracksresults include the album'srow, and tracks are sorted by disc number, track number and library order.library.addToPlaylist,playlist.addHandlesandplaylist.insertTrackscheck their paths as media reads; if any path is denied, the whole call fails withPERMISSION_DENIED.newOrderinplaylist.reorderandplaylist.reorderPlaylistsfails withINVALID_PARAMSif it lists an entry twice.- Track event payloads include
artistsandrating, and every field is always present. audio:dspPresetChangedandplaylist:defaultFormatChangeddeliver an empty object instead ofnull.- A query the parser rejects fails with
INVALID_PARAMSanddetails.param: 'query'inplaylist.getMatchingRows,library.searchandlibrary.query. - Paths longer than 259 characters fail with
OPERATION_FAILEDanddetails.value206;file.copyAsync,file.moveAsyncandfile.deleteAsyncreport the reasonpath-too-long. See Path length. file.readandfile.writeinclude the Windows error code indetails.valuewhen the file cannot be opened.- Lyrics
filenamevalues follow Windows file name rules: periods inside a name are allowed, and a trailing period or space is refused. - Saving lyrics invalidates cached reads.
- Band spectrum output is kept for compatibility with unchanged values, and the documentation lists it as legacy.
- When the host rejects an
fb2k.invokerequest itself, theErrorcarries acodeproperty (such asMETHOD_NOT_FOUNDorINTERNAL_ERROR). - In the SDK,
library.getAllresolves withOPERATION_FAILEDon failure or timeout instead of rejecting;playlist.getTracksPageis deprecated, asgetTracksreturns the same page. - A
|subsong:Nsuffix whose index cannot be read is dropped, and the path refers to the first track. audio.generateWaveformis removed (every call used to fail as not implemented); useaudio.generateFullWaveform.
Fixes
- Fixed an issue where popups and panels did not receive
menu:select,menu:dismissandmenu:valueChanged; these events now go to the page that opened the menu. - Fixed an issue where a popup or panel that started a JIT session did not receive
jitQueue:*events, so playback stopped after the first track. - Fixed an issue where panel pages did not receive
http:responseandhttp:downloadComplete. - Fixed an issue where panel pages did not receive
app:beforeQuitwith Default UI or Columns UI as the interface. - Fixed an issue where several panels in the same foobar2000 window were identified as the same panel.
- Fixed an issue where
window.getModereportedwindowIdasmainin popups. - Fixed an issue where
<fb-titlebar>and<fb-window-controls>in a popup followed the main window's maximize state. - Fixed an issue where a popup stayed on the browser's error page when the development server could not be reached.
- Fixed an issue where a URL with a user name or password could pass for a trusted origin.
- Fixed an issue where
LYRICISTtags were read as lyrics. - Fixed an issue where
playback.playPathsplayed paths it reported as skipped. - Fixed an issue where
playlist:addCompletecounted tracks refused by a locked playlist inaddedCount. - Fixed an issue where
playback:stateChangedreportedplayingwhen playback started paused. - Fixed an issue where text that is not valid UTF-8, such as an internet radio stream title, stopped an event from being sent; invalid bytes are now replaced with U+FFFD.
- Fixed an issue where
metadb:changedomitted the playback statistics rating for tracks without metadata. - Fixed an issue where
queue.getreported a wrong playlist position after rows or playlists were removed or moved. - Fixed an issue where
selection.getViewingTrackreported the active playlist without a row for a selection made outside it. - Fixed an issue where
playlist.playTrackwithdeferred: trueplayed the wrong row if playlists were added or removed before playback started. - Fixed an issue where the SMP layer's
RemovePlaylistSelectionwithcropcleared the whole playlist when the selection could not be read. - Fixed an issue where
discovery.executeContextMenuByPath, thetitleformat.evalmethods andreplaygain.get,clearandscancould not target tracks after the first in a multi-track file (|subsong:N). - Fixed an issue where failed rows of
playcount.getandgetBatchdid not echo the requested path. - Fixed an issue where
taskbar.flashreported failure when the main window was in the background. - Fixed an issue where ports opened by a page stayed open after the page reloaded or navigated away; they now close with
port:disconnected. - Fixed an issue where
fb2k.onreturnedundefined; it now returns a function that removes the listener. - Fixed an issue where a drag-out refused by the host also cancelled the drag inside the page.
- Fixed an issue where a long drag from Explorer could leave the drag image stuck and pages unresponsive.
- Fixed an issue where a file dropped where the page did not handle it opened in a separate window.
v1.14.0 (2026-10-06)
Breaking changes in this release
library.getArtistAlbums,getArtistTracksandgetAlbumTracksnow match exactly: comparison is byte for byte and case-sensitive, and*and?are no longer treated as wildcards. To keep the previous substring match ingetArtistAlbums, passmatch: 'substring'.- The
artistfield oflibrary.getArtistAlbumsrows now reports the album artist (the firstalbum artistvalue, or the firstartistvalue when that tag is absent). library.getArtistAlbumsnow sorts before applyinglimit, so a truncated result may contain different albums;limit: 0returns an empty list.- For queue entries without a playlist position,
queue.getreportsplaylistandplaylistItemasnull; testitem.playlist == null.
New features
- Tracks can be dragged out of the window as real files: exchange paths for a one-time token with
dnd.prepareDrag, then write it into the drag indragstart(fb.dnd.applyDragTokenin the SDK).dataTransfer.effectAllowedmust be'copy'. - New
dnd:dragEndedfires only when the host refuses to attach the files; subscribe withfb.dnd.onDragEndedin the SDK. dnd.getCapabilitiesanddnd:capabilitiesChangedincludedragOutanddragOutUnavailableReason; dragging out requires WebView2 Runtime 144 or later.- New
queue.setContentsreplaces the playback queue with an ordered list. - New
queue.insertNextqueues tracks, given by path or playlist position, to play next; a track already queued is moved rather than queued twice. - New
queue.playNowplays a queued item immediately. playback:queueChangedincludescount, and a rebuild of the queue is broadcast once.- New
playlist.getGroupRunsreturns the group boundaries of an entire playlist without its rows. playlist.getTracksacceptsfieldsto return only the specified fields; unknown names fail withINVALID_PARAMSand are listed indetails.unknownFields.library.getArtistAlbumsacceptssortandmatch, and its response includesartist,totalandhasMore. Its rows include the full set of album fields (albumArtist,discCount,duration,genre,label,firstTrackPath,firstTrackAbsolutePath), with counts covering only the artist's tracks.library.getArtistsacceptsincludeAlbums; whentrue, each artist includes its albums.- Lyrics files named
<artist> - <title>.lrc, and shared.lrcfiles without the subsong number, are now found. - Spectrum adds
scale: 'db',minFrequencyandmaxFrequency, and thefftSizelimit is raised to 65536. Each frame carriessubscriptionId, the parameters in use,streamTimeandhostTime. audio.getWaveformacceptschannelsandpoints.- Full-track waveforms are no longer decoded again when
method,signedorscalechanges, and concurrent requests for one file share a single decode. The newaudio.cancelFullWaveformcancels a pending task, and responses includemaxAmplitude. - SDK: the function returned by
fb.audio.subscribeSpectrumcarriesready;fb.audio.generateFullWaveformacceptssignal; newplaylist.getGroupRunsandplaylist.getTracksPage;library.getArtistAlbumsacceptsoptions. dialog.openFolderacceptsdefaultPath, including%music%.- Rating, slider and segmented rows in self-drawn menus can be adjusted with the mouse wheel.
- The preferences page (
Display → WebView2 UI) is redesigned: changes are saved whenApplyorOKis clicked,Reset pagerestores the defaults, andCanceldiscards changes. - New Window, Performance and Developer pages provide settings for default zoom (50–200%), preloading WebView2 at startup and the CDP port (1024–65535, default 9222).
- Templates are created, renamed and deleted under
Manage...; these operations take effect immediately.
Changes
- Background mode, deep suspend, developer tools, CDP remote debugging, and the development server switch and URL have moved from Advanced to the new pages, and existing values are carried over. Local network access over HTTP and invalid TLS certificates remain under
Advanced → Tools → WebView2 UI. - Changing the component language is now written on
Apply; dialogs opened afterwards use the new language. - The active template and templates referenced by a panel cannot be renamed or deleted, and an empty template list no longer creates
defaultautomatically. - Spectrum frames are pushed on time at each subscription's frame rate; on pause or stop one silent frame is sent before frames stop; out-of-range parameters fail with
INVALID_PARAMS. durationofaudio.getWaveformmust be within (0, 1] seconds, or the call fails withINVALID_PARAMS.- While hidden to the tray,
window.getStatereportsminimized: true, and hiding sends one morewindow:stateChanged. Allow insecure HTTP connections (disable HSTS)is marked as reserved and currently has no effect.
Fixes
- Fixed an issue where lyrics and cover files in a track's folder could not be read, including in portable installations (#15).
- Fixed an issue where tracks whose file names contain
..were rejected as path traversal (#17); thefilenameoflyrics.saveandmetadata.embedArtworkand thefileoflog.writefollow the same rule. - Fixed an issue where artists whose names contain a double quote returned no albums or tracks.
- Fixed an issue where
library.getArtistAlbumsundercountedtrackCountwhen truncated and omittedalbumson failure. - Fixed an issue where
<fb-library-tree>listed other artists' albums under an artist node. - Fixed an issue where spectrum subscriptions changed each other's
fftSize,bandsandfps. - Fixed an issue where foobar2000 could crash when generating a full-track waveform for a track with a very long path, and different tracks could share one waveform.
- Fixed an issue where the area previously occupied by the window could not be clicked after hiding to the tray.
- Fixed an issue where tracks added by native path (such as
E:\...) did not match the same file in a playlist. - Fixed an issue where
playlist.addPathsSequentialdid not preserve the given order. - Fixed an issue where
playlist.addPathsAsync,library.addToPlaylistandaudio.analyzeBPMignoredpath|subsong:N. - Fixed an issue where a playlist row's
ratingcould differ fromrating.get; both now read foo_playcount first.
Known issues
- 1.x does not fully check where a page comes from: an
http(s)URL opened withwindow.createPopupis trusted outright, so the page can call every API,shell.execincluded; in development mode, pages on any local port are trusted too; and events are pushed to whatever page is loaded, including one that has navigated to another origin. These issues are fixed in v2.0.0 and will not be fixed in 1.x. Do not open untrusted URLs in popups. - Tracks queued through the
pathsoption ofqueue.insertNextcarry no playlist position; after they finish, playback resumes from where it was before the insertion. When the track's playlist position is known, useitemsinstead. - The playback queue is not kept across restarts; this is foobar2000's own behavior.
v1.13.0 (2026-08-26)
Breaking changes in this release
Four changes may require code edits:
library.searchno longer duplicates its row array asitems. The response used to carry the same rows twice, byte-identical, underitemsandtracks. It now sends onlytracks— the keylibrary.queryhas always used. Code readingresult.itemsshould readresult.tracksinstead; every response shape — on older hosts too — carriestracks, so atracks-first reader keeps working against hosts that still send both. This halves the payload of every row-carrying search response.dialog.confirmresolves with{ response }, not{ confirmed }. The declared type was wrong. The host has always returned the zero-based index of the clicked button inbuttonsand never aconfirmedflag, so TypeScript code readingresult.confirmedwas reading a field that was never populated. With the default button set['OK', 'Cancel'],0means confirmed and1means cancelled. The task dialog is created withoutTDF_ALLOW_DIALOG_CANCELLATION, so Escape and the close button do not dismiss it and every result comes from an actual button click;-1appears only on the host's fallback path, when even a plain message box could not be shown.file.*error messages changed, and a rejected path is no longer echoed back. Every hand-rolled error envelope in the namespace now goes through the standard error envelope, and astd::filesystemexception surfaces only its Win32 error number — the exception text and the offending path no longer reach the payload or the host log. A path-security refusal now readsfile.read: path security denied for 'path': Access denied: protected system path: it names the method, the parameter (with its index for array parameters, as initems[2].destination) and the policy reason, so a caller can tell which argument was refused without the host leaking a filesystem location into a payload a page may forward elsewhere. Code that parsedresult.errorfor a path or for specific wording has to switch toresult.codeplusresult.details.- A parameter of the wrong shape now returns
INVALID_PARAMS, notPERMISSION_DENIED. The path-security decorator previously reported a refused path and a malformed argument under the same code. A handler that branched onPERMISSION_DENIEDto catch type errors will stop seeing them there.
Asynchronous file operations
- New:
file.copyAsync,file.moveAsync,file.deleteAsyncandfile.cancelOp. The work runs on a host worker thread, so copying a large album no longer freezes the UI the way the synchronousfile.copydoes. Each call returns a{ operationId, totalCount }receipt immediately; the outcome arrives onfile:opProgressand is followed by onefile:opComplete. - New events
file:opProgressandfile:opComplete(payload:operationId,op,done,total,results/ the three counts andcancelled). Results are batched, never one event per entry: they accumulate until 64 are pending or 100 ms have passed since the previous batch, so a fast run collapses to roughlyceil(total / 64)events while a slow one keeps a progress signal moving.done / totalis a usable progress fraction, and the final partial batch always arrives beforefile:opComplete. - One result is reported per requested entry, never per file: a directory entry is reported once its whole tree has been walked. Each result echoes
source(anddestination, except fordeleteAsync) exactly as requested,%variable%placeholders included and unexpanded, so it works as a lookup key.statusis'ok'/'skipped'/'failed', withreasondrawn fromalready-exists,not-found,permission,cross-volume,io-errorandcancelled. - Path validation is all-or-nothing. If any entry fails the host's read or write check the whole call is rejected with
PERMISSION_DENIEDand nooperationIdis produced — a partial batch is never dispatched.copyAsynccheckssourceasReadanddestinationasFileWrite;moveAsyncchecks both ends asFileWritebecause a move deletes the source;deleteAsyncchecks every path asFileWrite. At most 8 operations may be in flight process-wide. - Copy and move differ on an existing destination:
copyAsyncmerges a directory into an existing directory (files already there are skipped without being reported individually, and the entry still reportsstatus: 'ok'), whilemoveAsyncreports it asskipped/already-exists.overwrite(defaultfalse) covers file destinations only — Windows cannot swap a directory in place, so an existing directory destination is never replaced. Note that the synchronousfile.movealways replaces a file destination; the async form does not unless asked. - Within one volume a move is a rename and costs nothing regardless of size. Across volumes the host falls back to copy-then-delete-source; the entry still reports
status: 'ok'but carriesreason: 'cross-volume'so the extra cost is visible. deleteAsyncdefaults tomoveToTrash: true, which hands each path to the shell and therefore needs the main thread — those deletes run there in batches of 16 and yield in between.moveToTrash: falsedeletes on a worker thread and removes non-empty directories, which the synchronousfile.deleterefuses to do in that mode.file.cancelOptakes effect part-way through a batch rather than at the end of it: a copy or move stops within one file, aborting the file in flight and removing its partial copy, and a delete stops at the next entry. Entries already done keep their results, every remaining entry is reported asskipped/cancelled, and the run still ends with afile:opCompletecarryingcancelled: true. Closing a popup cancels the operations that popup started; a panel host has no such hook, so its operations run to the end unless cancelled explicitly.cancelled: falsecomes back when the operation had already finished or never existed — the two cases are deliberately indistinguishable.- Both events go to the window that made the call while that window is alive, which is what makes it safe for
resultsto carry real filesystem paths. Once the window is gone the host can no longer resolve it and falls back to the main instance, so a late event may surface in a window that did not start the operation. Two paths skipfile:opComplete— the host shutting down mid-run, and an unexpected host-side failure — so a listener that must not leak state should carry its own timeout rather than wait on it forever.
Metadata probing
- New:
metadata.probeBatchAsyncandmetadata.cancelProbe. Reads happen on a host worker thread, so a few hundred paths no longer stall the UI the waymetadata.readBatchdoes. The call returns a{ operationId, totalCount }receipt; results arrive onmetadata:probeProgressand are followed by exactly onemetadata:probeComplete. - Each result reports where its info came from —
infoSource: 'cached' | 'direct'— which is what makes the endpoint useful for files the media library has never seen. On failure it reports which of'not-found'/'unsupported-format'/'read-error'applies, a distinctionreadBatchcollapses into one generic error string.includeTags(defaulttrue) attaches the flat tag map, with upstream keys upper-cased as inreadBatch; passfalsewhen only technical info is wanted. - Paths may carry a
|subsong:Nsuffix, are resolved independently and are echoed back verbatim so they work as lookup keys. Unlikemetadata.read, the batch surface does not honour the legacy#Nsubsong spelling, which would mis-split an extensionless filename that happens to end in#<digits>. - Path validation is all-or-nothing: if any path fails the host's media read check the whole call is rejected with
PERMISSION_DENIEDand nooperationIdis produced. Per-path rejection is not available. metadata:probeProgressbatches on the same 64-or-100 ms rule as the file events.metadata:probeCompletealways arrives, on the cancelled and failed paths too. Cancellation interrupts the in-progress disk read rather than waiting for it; paths not yet reached are never reported and the interrupted path is reported as neither success nor failure, sosuccessCount + failureCountfalls short oftotalon a cancelled run.
Drag and drop
- A dropped shortcut now tells you what it points at. Windows puts the
.lnkfile itself in a dropped file list, which foobar2000 cannot play, so every path source is now joined by a parallel array of shortcut targets:resolvedPathson thednd:enter/dnd:droppayloads and ondnd.getPathsAsync(), plus the newdnd.getResolvedPaths()for the synchronous snapshot. The two arrays are always the same length, sopaths.map((p, i) => targets[i] ?? p)gives a playable list. - An entry is
nullwhenever no target is available: the path is not a shortcut, the shortcut names a shell namespace object such as the recycle bin rather than a file, the recorded target is too long to come back intact (Windows caps it atMAX_PATH, and a truncated path would name a different file), COM was unavailable, or resolution was skipped to keep the drop responsive. Never an empty string, so a truthiness test is enough. - A target says where the shortcut points, not that the file is there. A broken shortcut reports the path its
.lnkrecorded rather thannull, because Windows hands that path back whether or not the target still exists and the host cannot afford a filesystem check on the thread the drag blocks. Expect a non-null entry to occasionally name nothing. Only.lnkis resolved —.url,.library-msand virtual search results reportnull. - Reading
resolvedPathscosts no filesystem access: the targets were resolved once when the drag arrived, sogetPathsAsync()reads host memory only. - A known limitation — the page-side snapshot is published to the top-level document only, so
dnd.getPaths()anddnd.getResolvedPaths()answer with an empty array inside an<iframe>and the slot staysnullfor the life of that document. A framed page that needs paths has to receive them from the main frame overpostMessage. - Dragging tracks out of the window is still unsupported and
dnd.startDragkeeps resolving{ success: false, code: 'NOT_SUPPORTED' }.
Media library queries
- Multi-value tags are no longer truncated to their first value. Since the first release, a track tagged with several artists reported only the first one everywhere outside
metadata.read. Track objects now join the values with,in tag order, without de-duplication — matching what foobar2000 itself displays — forartist,albumArtist,genreandcomposer, each API keeping its own field set. - New:
artists, the atomic form ofartist. A joined string cannot tell one artist named"A, B"from two artistsAandB, so track objects from the library namespace —library.getAll,library.query,library.search,library.getByPathand the otherlibrary.*track endpoints — also carryartists: string[]with the untouched values;artists.join(', ') === artistholds byte for byte. Track objects from other namespaces (playlist.getTracks,playback.getCurrentTrack,queue.get, artwork and event payloads) do not carry the field. - Aggregation counts every credited artist.
library.getArtistsgives each participating artist its own entry, which makestrackCounta participation count: the entries add up to more than the number of tracks, andalbumCount/totalDurationare counted per artist the same way.library.getStats'stotalArtistsnow matches the entry count ofgetArtists, andlibrary.getGenrestreats each value of a multi-valuegenreas its own entry. - New: field projection on
library.searchandlibrary.query.library.searchacceptsoptions.fieldsandlibrary.querygained a fourthfieldsargument. Every returned row then holds exactly the requested keys and nothing else, soTrackInfois a partial view at runtime while the declared type stays complete. Accepted names are the 20 track keys —index,title,artist,artists,album,albumArtist,genre,date,trackNumber,discNumber,duration,path,absolutePath,fileSize,bitrate,sampleRate,channels,codec,subsong,rating— matched case-sensitively. Omit the argument to get all 20. A non-array, an empty array, a non-string element or an unknown name resolves (never rejects) with{ success: false, code: 'INVALID_PARAMS' }and echoes the offending names underdetails.unknownFields. library.searchandlibrary.querynow serialize off the main thread and write their result straight to the wire instead of building an intermediate object graph. The response pipeline stops deep-copying along the way, and a query with no hits short-circuits. Aside from theitemsremoval listed under breaking changes, the JavaScript contract is unchanged: both still resolve the same shape from the same promise.- The Spider Monkey Panel compatibility layer stops re-scanning the library once per page.
fb.GetQueryItemsused to page through hits 500 at a time, and becauselibrary.searchre-scans the whole library on every request, a multi-thousand-hit query paid one full scan per page — 160 scans on a large library. It now issues a one-row probe for the hit count followed by a single request for every hit, narrowed to the five keysFbMetadbHandleactually reads. No observable handle property changes. Trade-off: the probed count is stale if the library changes between the two calls, so hits added in between are dropped; the returned set is not guaranteed stable while the library is being modified concurrently. - Practical note on very large libraries. These changes remove copies and payload, not the cost of producing the rows. A full-field whole-library query still occupies the host's main thread for a substantial time on a six-figure library, dominated by the size of the response being handed to the page. Narrow the projection with
fieldswhen you do not need all 20 keys — it is the single most effective lever available today. On 32-bit hosts also keep the peak in mind: a full-field whole-library result is held in several forms at once while it is parsed, so a six-figure library can reach hundreds of megabytes momentarily.
Title formatting
titleformat.eval,evalBatch,evalFieldsandevalFieldsBatchnow reportinfoAvailable. The host knew whether a track's metadb info was ready and was throwing that signal away, so tag-derived output could come back silently wrong.infoAvailable: falsemeans tag-derived values are untrustworthy. Batch variants carry the flag per row, and rows that failed omit it.- Two limits worth knowing: one flag covers the whole merged script in the
evalFieldsforms, so it cannot tell you which individual field was affected, and it never covers foo_playcount virtual fields. Afieldskey literally namedinfoAvailableoverwrites the flag, matching the existing behavior ofpathandsuccess.
Errors and permissions
fb2k.invokeinside a subframe now fails immediately. It rejects with anErrorcarryingcode: 'NOT_SUPPORTED'and the messagefb2k.invoke is unavailable in subframes, instead of hanging silently until the 30-second timeout. A detached call (const { invoke } = fb2k) still surfaces itsTypeErroras a rejection rather than a synchronous throw.- A cross-volume directory move now returns
NOT_SUPPORTEDwithdetails.reason: 'cross-volume'rather than a generic failure. A cross-volume file move already succeeded, because the underlying rename silently degrades to a copy. - Path allowlist and blacklist entries are now normalized to the same canonical real-path form the check side uses. An entry written in a different but equivalent spelling — a mapped drive, a junction, a short 8.3 name — previously failed to match the path it was meant to cover.
- Dropping a shortcut, probing metadata and the async file operations are all covered by the permission matrix in the reference; the counts there moved from 67 specs over 64 APIs to 73 over 68.
FileWrite now accepts media-library watch folders
The FileWrite chain — the channel behind every file.* write — gained a media-library watch-folder step, so a path inside a watch folder is now writable even on the system drive, where previously only the non-system-drive step would have allowed it. FileWrite was already the widest write channel exposed to a theme; this widens it further. If you audit themes, this is the channel to look at first, and the watch-folder list is now part of its attack surface.
Window and menus
- Collapsing a menu no longer leaves an invisible click trap. The WebView rendering area is now shrunk in the same step as the menu closing, so no residual transparent region is left behind intercepting clicks meant for the page underneath.
SDK
- New exported types for the async file surface:
FileOpEntry,FileOpAsyncOptions,FileDeleteAsyncOptions, plus payload types for the four new events (FileOpProgressPayload,FileOpCompletePayload,MetadataProbeProgressPayload,MetadataProbeCompletePayload) and their supporting unions (FileOpKind,FileOpStatus,FileOpResultReason,MetadataProbeFailure,MetadataProbeInfoSource). dialog.openFile,saveFileandopenFoldernow document their resolve shapes ({ canceled, filePaths }/{ canceled, filePath }/{ canceled, folderPath }), where the path field is empty on cancellation.dnd.getPathsAsyncis typed against the shared session-paths shape soresolvedPathsis visible to TypeScript.dnd.getResolvedPaths()pads frompaths, so a host that predates the field yields nulls of the right length rather than a short array that would silently misalign an index-paired loop.
Documentation
- Every parameter table in the bilingual API reference now carries a Default column, and filler prose that restated the parameter name has been removed throughout.
- Required-column entries were verified one by one against the C++ handlers' null checks, correcting roughly 50 mismarked parameters across both languages.
library.coverMaxSizeis documented in KB, not bytes. Three brokenlibraryreturn-field tables were rebuilt against the actual host structures. Twelve findings from an adversarial documentation audit were applied, including two reversed semantics and several misleading descriptions.
v1.12.0 (2026-08-13)
Breaking changes in this release
Four changes may require code edits:
- The
dnddrop-zone registry is gone.dnd.registerDropZone/unregisterDropZone/getDropZonesno longer exist; the host now observes drags natively and emitsdnd:enter/dnd:leave/dnd:dropto the window under the cursor, no registration required. Read real paths withfb.dnd.getPathsAsync()or from thednd:droppayload. dnd.startDragno longer fakes success. Dragging tracks out of the window needs a nativeIDropSourcethe component does not provide; the call now resolves with{ success: false, code: 'NOT_SUPPORTED' }instead of reportingsuccess: true.- Window size constraints target the calling window. The six
window.setMinSize/getMinSize/setMaxSize/getMaxSize/setResizable/isResizableendpoints no longer fall back to the main window, and a call that resolves no target now fails. A popup that relied on the old fallback was constraining the main window. DiscoveryContextMenuCommandis no longer a type alias. Readingpath/isDynamic/subGuidoff a context-menu command no longer type-checks — those fields were never populated.
Drag and drop
- Drag and drop is now a native pipeline. The host observes drag gestures itself through a native
IDropTargetbridge and hands the page what HTML5 deliberately hides: real filesystem paths. Standard HTML5 drag events keep firing as before;fb.dndruns alongside them as a side channel. - New surface:
dnd.getPathsAsync(sessionId?)(the reliable read inside adrophandler),dnd.getPaths()/dnd.hasFiles()(synchronous snapshot reads for optimistic UI), anddnd.getCapabilities()(whether this window can deliver paths at all). - Events
dnd:enter/dnd:leave/dnd:drop(payload:sessionId,paths,x,y,keyState) anddnd:capabilitiesChanged, correlated bysessionIdand delivered point-to-point to the window under the cursor. - Paths are withheld from untrusted origins while
hasFilesstays accurate; a DUI / CUI panel (hosting: 'standard') cannot receive paths — branch ongetCapabilities()rather than assuming from the window type. - Migration: delete
registerDropZone/unregisterDropZone/getDropZonescalls and anyzoneIdbookkeeping; keep (or add) plain HTML5dragover/droplisteners for visuals and hit-testing; read real paths withawait fb.dnd.getPathsAsync()inside thedrophandler; gate path-dependent UI ondnd.getCapabilities().
Main menu
menu.runMainMenuCommandno longer leaks host exceptions. On localized foobar2000 builds a host exception previously escaped to JavaScript as a raw host-languageErrorand made the name and path forms fail outright. Failures are now reported assuccess: falsewith acode:MENU_ITEM_DISABLED,MENU_MATCH_AMBIGUOUS(withcandidates), orMENU_COMMAND_NOT_FOUND.menu.getMainMenuleaves are addressable on localized hosts. Leaves are now matched against the same text the host rendered the menu from and backfilled withguid/subGuid; measured 0 of 158 leaves before, 131 of 167 after on a localized host.flags,enabled,checked, andhiddenare now reported as well, where previously none of them were.- Behavior change — a disabled command is refused instead of reporting success. Execution previously returned
success: truefor a greyed-out command, and the same command was refused by name yet "succeeded" by GUID. All three request forms now validate alike and returnMENU_ITEM_DISABLED. A GUID absent from the enumeration is still attempted, because a caller may hold a valid address the enumeration did not surface. - Name and path resolution matches by exact segment. An ambiguous name is reported rather than resolved: on a localized host three separate commands can share one label, so picking the first match would silently run the wrong command. Address by
guidto be unambiguous — it is the only form stable across hosts, since a localized build reports localized labels. menu.runMainMenuCommandacceptssubGuidto address a dynamic child command, paired with its owning command GUID.- A leaf that could not be resolved to an address now says so, carrying
executable: falseandunaddressableReasonrather than appearing as an ordinary command the caller cannot act on.availableon flat enumeration results is read from live host state instead of being alwaystrue, so a disabled command is no longer indistinguishable from an enabled one.
Self-drawn menu
- Per-call presentation options.
menu.show/menu.popuptake a thirdMenuPopupOptionsargument, and keys the caller omits are not sent, so the host keeps its own defaults:windowModel('fullscreen'default, or'contentSized'— draws the root and its first-level submenu as separate compact windows measured to their content, so each panel carries the real DWM backdrop material and the system window shadow; the recommended model for a context menu),css(at most 256 KiB) /cssReplacefor style takeover,backdrop('acrylic'default,'mica','mica-alt','none') withbackdropDarkMode(defaulttrue), andcloseAnimationMs(default0, clamped to0..1000) for an exit fade. - Rich items.
MenuPopupItem.typegains'nowplaying','rating','slider', and'segmented'along with the fields they use (value,min/max/orientation,segments, andcover/title/subtitle), plusiconSvgfor an inline monochrome icon on any row. Icons go through the runtime's allowlist sanitizer; an illegal or oversized one is dropped without failing the row. - New event
menu:valueChangedreports a rating, slider, or segmented change as{ menuId, itemId, value }and keeps the menu open, while ordinary rows still report throughmenu:selectand close it. Becausemenu.popupresolves only on selection or dismissal, subscribe to this event separately when a menu contains value controls. - Fixed — focus-loss dismissal is now reliable, and pooled submenu windows no longer show blank content in the
contentSizedmodel.
Discovery menus
- Behavior change —
discovery.searchCommandsnow searches the context menu as well as the main menu, so result counts increase andtypecarries a new'contextmenu'value. Pass{ scope: 'mainmenu' }for the previous coverage. The endpoint previously reportedtype: 'mainmenu'on every hit while only ever looking at the main menu, which made right-click commands unfindable. - Behavior change —
discovery.searchCommandsfilters entries the host would not show, matching the enumeration endpoints. Pass{ includeHidden: true }for the unfiltered superset. - Behavior change —
discovery.getAllServicescounts context-menu commands inservices.contextMenuCommandsand includes them intotalServices, sototalServiceschanges value.contextMenuHiddenFilteredreports how many entries the filtering removed, andstateKnownis false when nothing was selected or playing. - Search hits carry the same state fields as the enumeration endpoints (
enabled,checked,radioChecked,hidden,stateKnown,flags,source,executable,unaddressableReason), so a caller can tell whether a hit is invocable without a second round trip. When the context family was searched without a track selected or playing, the response'sstateKnownis false and those hits'enabled/checkedmust not be filtered on. - Search case folding is now ASCII-only and can no longer corrupt a UTF-8 sequence. Observable matching behavior for CJK labels is unchanged — they have no case to fold.
discovery.getContextMenuTreeno longer truncates silently. Children were previously capped at 50 and depth at 10 while still reporting the host's realchildCount, sochildren.lengthdisagreed withchildCountwith nothing explaining the difference. The limits are now depth 16 and 512 children per node, and any clipping is reported: apopupnode gives bothchildCountandchildrenReturned, and a node whose subtree was clipped carriestruncatedwithdepthExceeded/childrenExceedednaming the cause. The flags propagate upward, so the response's top-leveltruncatedcovers the whole tree;maxDepthandmaxChildrenPerNodeecho the applied limits.discovery.getContextMenuTreenodes now report state —enabled,checked,radioChecked,hidden,stateKnown,flags— anddepth. Separators carry only their kind, which is all that is meaningful for them.discovery.executeContextMenuCommandreturnshiddenandresolvedon the success path, not only when refusing, so a caller can tell "was not refused" apart from "this build does not report the field".resolvedis false when no registered item owns the GUID, in which case there was no state to evaluate.discovery.getMainMenuCommandsentries expanded from a dynamic submenu now carrystateKnown,executable, andunaddressableReason, matching the static slots.
SDK
- Breaking type fix —
DiscoveryContextMenuCommandwas a type alias forDiscoveryMainMenuCommandand therefore claimed fields the context tier never returns. It is now an independent interface: context items are registered flat and placed by the host, so they have no menupathand no dynamic-expansion fields. TypeScript code that readpath/isDynamic/subGuidoff a context-menu command was reading a field that was never populated. - Added the exported
MenuNodeState,MenuNodeSource, andMenuUnaddressableReasontypes, shared by every menu enumeration result. fb.discovery.searchCommands()accepts{ scope, includeHidden };fb.discovery.getContextMenuCommands()accepts{ includeHidden }.- Fixed SMP main-menu dispatch. A menu id was previously resolved by
pathbeforeguid, and path matching fails outright on localized hosts, soExecuteByIDon a main menu did not work there at all.guidis now preferred (the host resolves it directly);commandIdstill wins for context-menu sessions. - Fixed SMP menu state decoding.
enabled/checkedwere previously re-derived from the rawflagsword, ignoring the normalized booleans the host sends. Those booleans are now preferred, withflagsused only when they are absent. An item markedstateKnown: falseis offered as enabled rather than greyed out, because "state unknown" and "enabled, unchecked" are indistinguishable inflags, and treating unobserved state as disabled hid commands the host would have run.
Tray and menus
- Fixed — the tray menu's "show main window" path no longer depends on the main page. Once the window was minimized or hidden to the tray, the page was deep-suspended, so no
tray:menuItemClickedhandler could run and nothing could callwindow.focusto bring the window back. A left click on the icon was equally silent. Native items (_sys_exit,playbackAction) kept working throughout. - Behavior change —
showSystemItems(defaulttrue) now injects_sys_show("Show Main Window") before_sys_exitin the bottom zone. It restores and foregrounds the main window natively, preserving its maximized / normal placement, and like every native item it does not firetray:menuItemClicked. Menus built withshowSystemItems: truetherefore gain one row; passshowSystemItems: falseto opt out. _sys_showjoins_sys_exitin the exact, case-sensitive reserved-id allowlist, so a frontend that renders its own "show main window" row gets the same native route with itslabel/iconpreserved, and the injection is skipped for it. Lookalikes such as_sys_show_altor_SYS_SHOWremain ordinary user items and do not suppress the injection.playbackActionstill rejects'show-main-window'and'exit': system routes stay exclusive to the reserved ids.
Window
- Behavior change — the six size-constraint endpoints (
window.setMinSize/getMinSize/setMaxSize/getMaxSize/setResizable/isResizable) now target the calling window, or an explicitwindowId, and no longer fall back to the main window. A popup that set its own minimum size previously constrained the main window instead. When no target resolves the call fails rather than reporting another window's constraint. DUI / CUI panel callers are refused withpanelMode: true. - Documentation fix — sizes on these endpoints are physical pixels, not DIP as previously documented. A caller that scaled by the device pixel ratio was applying the factor twice on a high-DPI display. A value round-tripped through a setter and getter is accurate to ±1px.
Interface language
- The plugin's native UI now follows foobar2000's presentation language instead of the Windows UI language, so a localized foobar2000 no longer shows an English preferences page. The language is detected from the host's own strings and falls back to the Windows UI language when detection is not possible.
- Added the Interface Language preference: Auto (follow foobar2000) (default), English, or 中文. Newly opened dialogs apply the change immediately; menu titles and panel descriptions are registered with the host once and need a foobar2000 restart to refresh.
Playlists
- Playlist files from third-party components now expand correctly. Adding a playlist URL or file through the API used a hardcoded list of eight wrapper extensions (
.pls/.m3u/.m3u8/.asx/.wpl/.xspf/.fpl/.cue); a playlist format registered by another installed component was treated as a single track. The host's playlist-loader registry is now consulted at runtime, so any format the host can load is expanded.
Performance and stability
- Fixed — the window no longer stays blank after the WebView2 browser process dies. Only the render process had a recovery path, so an external cause such as a third-party hook injected into the browser process left the window blank with no way to recover; one observed session ran that way for ten hours. The window is now rebuilt, capped at 3 rebuilds within a 10-minute window so a reproducible crash cannot degenerate into a rebuild→crash→rebuild loop. The window expires and the count resets, so an unrelated later failure is still recovered. This is separate from the v1.11.0 render-process handling, which reloads the page and rebuilds the WebView.
v1.11.0 (2026-07-27)
Tray and menus
- Added
TrayMenuItem.playbackAction('play-pause' | 'previous' | 'next' | 'stop') so a custom tray item can declare a playback action the plugin runs natively. Appearance stays caller-controlled; declared items do not emittray:menuItemClicked(same pattern as Electronrole/ TauriPredefinedMenuItem). Valid only on atype:'normal'leaf; unknown tokens or declarations on separator / submenu / rich controls reject the wholesetContextMenu/appendMenuItemscall withINVALID_PARAMS.'exit'is not accepted. Tray-only — no effect onmenu.show.getMenuItemsround-trips the field. Prefer this (or built-inshowPlaybackControls) for background-reliable tray playback while the main page is hidden; plain click→playback.*handlers are not guaranteed then. Available from v1.11.0; probeconfig.getVersionInfo().plugin.versionif you must support older hosts. - Clarified that
tray:menuItemClickedcovers ordinary user items and rich value controls only. Built-in playback / system injections and items declaringplaybackActionexecute natively without the click event; reflect button state fromplayback:*. menu.getMainMenuacceptslocale,i18n, andwithAvailability.locale(default'auto') selects thedisplayLabeltranslation locale and keeps the host's native labels untranslated by default;i18n: falsedisables label translation entirely;withAvailability(defaulttrue) includes per-submenu command availability counters. The SDK signature is nowgetMainMenu(root?, opts?).- Fixed UTF-8 serialization and context-mode selection for custom menus.
DSP and output
dsp.*andoutput.*now actually work. The eleven handlers (dsp.getChain/getPresets/getAvailable/addDsp/removeDsp/moveDsp/applyPreset/setChain,output.getDevices/getEntries/getSettings) were documented but their source files had never been added to the build, so every call failed as an unregistered method. They are compiled and registered from this release on. The publishedfb.dsp.*andfb.output.*SDK wrappers were already shipping and start working against this plugin version; older plugins reject them regardless of SDK version.- Fixed a crash in
output.getDevices. Some output backends report a device name with a "length unknown" sentinel instead of a real length; the handler used that value verbatim and read far past the end of the string, terminating foobar2000. - Fixed
dsp.moveDspmoving items to the wrong slot. Upward moves landed one position short, so moving an item up by one did nothing and no item could reach the end of the chain. Downward moves were already correct. The returnedtonow reports the real final index. - Behavior change —
dsp.setChainrejects a call that contains any unusable entry instead of silently skipping it. Previously a chain built from three entries could apply only two and still reportsuccess: true. Each per-entry failure now returns an index-tagged reason:dsps[0] must be an object(also for a non-object element such as a bare string or number),dsps[0]: guid is required(missing, empty, or not a string),dsps[0]: Invalid GUID format: …, ordsps[0]: DSP not found or no default preset: …(a well-formed GUID for a DSP that is not installed). A missing or non-arraydspsstill fails withdsps array is required. The chain is left untouched whenever a call is rejected. dsp.getPresetsreportsselectedIndex: -1when no preset is selected. It previously returned the internal sentinel18446744073709551615, which is not representable as a JavaScript number and arrived as an unusable float.dsp.getChainalways includesactivePresetandactivePresetIndex, usingnull/-1when no preset is selected or the host does not support presets. The keys were previously absent in those cases, so callers had to probe for them.
Discovery
- Behavior change —
discovery.getMainMenuCommandsanddiscovery.searchCommandsnow expand components that build their submenu at runtime (mainmenu_commands_v2, e.g. ESLyric), so results include child commands in addition to the parent slot. Pass{ expandDynamic: false }for the previous static-registry-only result. - New entry fields:
path,isDynamic,isDynamicParent,subGuid, andflags.getMainMenuCommandsechoesexpandDynamicanddynamicCount, anddiscovery.getAllServicesaddsmainMenuDynamicCommands. An entry flaggedisDynamicParentis a container slot and is not executable on its own. discovery.executeMainMenuCommandaccepts an optionalsubGuidfor commands expanded from a dynamic submenu; without it only the static command GUID is dispatched. The response echoessubGuidanddynamic.
Performance and stability
- Behavior change — while the page is hidden (minimized, covered, tray-hidden, or locked), high-rate regenerable streams stop at the source:
audio:spectrum,playback:time, andplayback:timeHighResare not produced, and resume on the next tick once the page is visible again.window:hoverStateChangedandcursor:hiddenChangedare naturally silent while hidden. Every other event is delivered reliably and in order — nothing is dropped or merged — including async replies such ashttp:response,library:getAllResult, andaudio:fullWaveformReady, and one-off facts such asplayback:itemPlayed. Themes that draw a spectrum or a seek position should read a gap as "page hidden", not as "playback stopped". - Added deep suspend while minimized, covered, locked, or tray-hidden: renderer timers and animations are frozen so the OS can reclaim memory. Controlled by the new advanced-preferences option Deep-suspend WebView when hidden (TrySuspend; frees renderer memory) (default on; turning it off falls back to the previous low-memory path).
- Added the advanced-preferences option Keep WebView active in background while CDP remote debugging is on (tray/minimize/lock) (default on), so screenshot and timing automation over the DevTools Protocol stays stable instead of being suspended.
- Hardened recovery after a WebView2 crash: a failed render process no longer leaves an unresponsive blank window. The page is reloaded up to a bounded number of attempts and the WebView is rebuilt after repeated failures.
- Improved album-art delivery: fixed cache entries that could serve another track's image, tightened request and parameter validation, and moved image decoding off the interface thread so large covers no longer make the window unresponsive.
artwork.*request and response shapes are unchanged.
Metadata
- Fixed
metadata.read,metadata.readByPath, andmetadata.readBatchignoring the track index inside a container. A|subsong:Nsuffix was neither stripped nor honored, so reading a single track out of a CUE sheet, ISO image, or multi-track file either failed outright or returned the first track's tags. This is why such files could be read in the foobar2000 UI but not through the API.metadata.readRawwas already correct. metadata.readandmetadata.readByPathacceptcueIndexto address a track explicitly, matchingmetadata.readRaw. It takes precedence over a|subsong:Nsuffix in the path. Thefb.metadata.read()/readByPath()wrappers take it as a secondoptsargument, and thefb2k_metadata_read/fb2k_metadata_read_by_pathMCP tools declare it.metadata.readBatchdoes not accept it — address per-track reads there with a|subsong:Nsuffix.
SDK
- Added SDK-only additive binary adapters:
fb.file.readBinary(),fb.file.writeBinary(),fb.file.writeDataUrl(),fb.metadata.embedArtworkBytes(), andfb.metadata.embedArtworkFromDataUrl(), plusFileBinaryWriteOptionsandMetadataArtworkBytesOptions. - These helpers adapt
ArrayBuffer/Uint8Arrayvalues and strict Base64 Data URLs to the existingfile.read,file.write, andmetadata.embedArtworkwire contracts. They add no new Bridge endpoints and do not change rawinvokeor existing facades. Canonical Base64 and Data URL validation occurs in the SDK before invocation; Host validation and behavior are unchanged. - Breaking type fix — the published response types for
ui.isMinimized()andui.isAlwaysOnTop()were wrong and now match the wire contract:isMinimizedresolves with{ minimized }(there is noisMinimizedalias), andisAlwaysOnTopresolves with{ enabled, isAlwaysOnTop }(both carry the same value). Runtime behavior is unchanged; TypeScript code written against the old declarations must be updated. fb.playcount.set()no longer sends thecountkey, which the host never read. No behavior change; the wire payload is simply smaller.fb.http.request()now dispatches through the documentedhttp.getendpoint; a stale internal parameter could previously forward a mismatched method name. The verb helpers (fb.http.post()/put()/delete()/patch()) keep dispatching to their own endpoints, including for binary responses.- Added
windowIdparameter typings forwindow.getBackdropPolicyandwindow.setBackdropPolicy.setBackdropPolicyrequiresbackdropPolicyand does not fall back to the main window when no target resolves. - Added optional trailing
optsarguments to five wrappers whose host handlers already read the corresponding keys:fb.file.delete(path, opts?)(moveToTrash),fb.file.copy(source, destination, opts?)(overwrite), andfb.metadata.write(path, tags, opts?)/removeField(path, field, opts?)/removeTag(path, tags, opts?)(cueIndex).metadata.writeacceptingcueIndexcloses a real gap: v1.11.0 wiredcueIndexinto the metadata read path only, so writing a tag to a single track inside a CUE sheet or image file was not expressible through the SDK. Existing call sites are unaffected — every new argument is optional. fb.metadata.readByPath()now resolves withMetadataReadByPathResponseinstead of a bareJsonObject.- Corrected two published type declarations that did not match the host contract:
dsp.setChaintakes a requireddspsarray of{ guid }objects (it was typeddsps?: string[], wrong in optionality, element type, and shape — the host rejects the call unlessdspsis an array, and readsguidoff each entry), and theplaylist:created/playlist:renamedpayloads keepname: string. TypeScript code written against the olddsp.setChaindeclaration must be updated.
v1.10.0 (2026-07-16)
- Added
TrayMenuItem.orientationfortype:'slider'with'horizontal' | 'vertical'; horizontal is the default. Only the exact valueverticalselects vertical behavior (min at the bottom / max at the top; Up/Right increase, Down/Left decrease, and Home/End select the bounds).nativeignores the field and keeps the tiered submenu; older runtimes ignore the unknown field and remain horizontal. Range normalization swapsmax<min;max==minis constant and emits no value; the initial value is clamped; out-of-range IPC values are rejected.getMenuItemsround-trips the field. Available from v1.10.0; themes that must support older hosts should probeconfig.getVersionInfo().plugin.version. - Changed custom-menu focus to two modes: navigation with roving tabindex and real focus, and rich-control editing. ARIA uses
menuitem,menuitemcheckbox, an internalrole=slider, and a segmentedradiogroup;checked:falseremains checkable. Default entrance and exit transform/transition effects are disabled underprefers-reduced-motion: reducewithout changing the hide protocol orcloseAnimationMs. - Added
TrayMenuConfig.layoutModewith'flat' | 'zones'. The default'flat'preserves direct#menu > .fb-itemchildren. Explicit'zones'creates.fb-zone[data-zone]wrappers for non-empty top / playback / bottom sections.nativeignores the field; older runtimes ignore the unknown field and create no wrapper;menu.showis unaffected. Available from v1.10.0; themes that must support older hosts should probeconfig.getVersionInfo().plugin.version. - Changed protected custom-menu CSS so the visible state no longer forces
#menu { display:block !important }. Themes can make the root menu or zones flex or grid containers, but cannot usedisplay:* !importantto reveal a hidden menu. - Hardened custom-menu SVG icons by replacing raw
innerHTMLinjection with DOMParser plus allowlisted element and attribute cloning. Invalid or individually oversized icons are discarded while the menu continues to render.transformis parsed strictly, rejecting prefixes, inter-function junk, and empty arguments, and nodes must be in the SVG namespace. - Added transactional resource-limit validation to
tray.setContextMenu,tray.appendMenuItems, andmenu.showbefore persistent configuration is written or an overlay opens: item ≤ 512,menu.showdepth ≤ 8, segmented options ≤ 64, CSS ≤ 256 KiB, and aggregate SVG ≤ 256 KiB. A single SVG over 32 KiB is discarded without rejecting the whole menu. Other invalid or oversized input returnsINVALID_PARAMSwithfield/limit/actualindetails; this is an intentional incompatibility for unsafe input. - Hardened tray and custom-menu built-in action routing to use trusted internal provenance instead of a public id prefix. The sole compatibility exception is the exact, case-sensitive
_sys_exitin the tray API, preserving the real exit behavior from 1.9.0. Caller-supplied_pb_playPause/_pb_prev/_pb_next/_pb_stopremain ordinary user items and cannot suppress runtime-injected trusted playback items through same-id deduplication. Opaque tokens distinguish duplicate public IDs, and publicmenu.showdoes not elevate them. Each selection or value change carries an unpredictable one-shot token validated against the current menu index; unknown or expired tokens, disabled items, and out-of-range rich values for rating / slider / segmented are rejected. Internalmenu.__*IPC also verifies that the caller is the overlay window and that select / dismiss / ready / submenuPanel / valueChanged match the current menu id; external callers and stale or forged menuId values are rejected without changing menu state. - Custom tray
ContentSizednow measures the root and every first-level submenu offscreen after fonts are ready, then waits for stable dimensions across two consecutive frames. C++ uses 64-bit-safe slot allocation. The fixed HWND region covers only the currently visible root/submenu panel, so reserved space for unopened panels no longer creates a blank acrylic/mica area and the caller's configured backdrop is not silently disabled. - Clarified that a
segmentedvalue change in a custom tray menu follows the keep-open contract. A segment change emitstray:menuItemClickedwith{ id, value }, wherevalueis the zero-based selected-segment index, and does not close the menu, matchingratingandslider. Thewebviewruntime already kept the menu open; this corrects the shared contract and event documentation that previously listed onlyrating/slider. - Fixed extra separators for completely hidden or empty tray-menu sections. Previously, filtering all items with
visible:falsecould leave a leading or trailing separator. Visibility is now filtered before separator decisions, so neither native nor webview menus render that separator. - Corrected the documentation for
TrayMenuItem.icon: base64 ICO remains reserved and neither backend renders it (nativeis text-only and webview rendersiconSvg). UseiconSvgfor menu-item icons.
v1.9.0 (2026-06-18)
- Added icons for normal and submenu items in custom tray menus (
render: 'webview') throughTrayMenuItem.iconSvg = { viewBox, content }. Inline monochrome SVG follows menu text color throughcurrentColorand uses a fixed 8px left-aligned gap. When any peer has an icon, all normal and submenu items reserve a 16px icon column for text alignment.nativemenus ignore it. - Added
config.autoNowPlayingtotray.setContextMenu. When enabled, empty cover/title/subtitle fields on anowplayingitem fall back to the current track when the context menu opens; caller-provided values take precedence. Thecoverfallback iswebview-only and uses a thumbnail of current artwork. title/subtitle use%title%with filename fallback and%artist%, including dynamic streaming titles. - Extended
TrayMenuItem.coverto accepthttp(s)://URLs in addition to existingdata:values and raw base64, allowing streaming frontends to pass live artwork directly. - Updated the SDK package to
1.9.0.
v1.8.0 (2026-06-10)
- Added custom-menu rendering through
menu.show/menu.close, with WebView-rendered content and recursive submenus. The menu window uses a content-sized fixed-window strategy to prevent expansion flicker. - Added
render: 'webview'totray.*, allowing tray context menus to use custom rendering consistent with the theme. - Added
tray.setMenuItemStateto update one menu item's state without rebuilding the entire menu. - Fixed clicks on always-on-top popups such as desktop lyrics occasionally bringing the main window to the foreground and making it topmost (the rollback path inserted z-order into the topmost band and formed a sink restoration reference loop).
- Fixed missing global
HTMLElementTagNameMapdeclarations in the published SDK so npm consumers regain type completion forfb-*custom elements. - Fixed package-script compatibility with newer PowerShell versions when generating
.fb2k-componentarchives. - Hardened the HttpApi asynchronous-request exception boundary and fixed a NUL string-handling defect in LibraryApi.
- Updated the SDK package to
1.8.0;bump-version.ps1now also synchronizessdk/package-lock.jsonand the VitePress navigation version.
v1.7.0 (2026-06-06)
- Added Taskbar & Tray capabilities.
taskbar.*can configure thumbnail-toolbar buttons, progress, overlay icons, and flash notifications;tray.*can create a system-tray icon, balloon notifications, and context menus. - Added incremental menu management to
tray.*throughappendMenuItems/removeMenuItems/clearMenuItems/getMenuItems, allowingtop/playback/bottomsections to be maintained without rebuilding the entire menu. - Added
taskbar:buttonClicked,tray:click,tray:doubleClick,tray:menuItemClicked, andtray:beforeContextMenuevents for taskbar-thumbnail and tray interaction. - Added
webview:processFailed, which broadcasts diagnostics for WebView2 render-process failures and works with automatic render-process recovery to reduce blank-window failures. - Added high-resolution playback-position event
playback:timeHighRes, driven by a dedicated WinAPI timer for sub-second lyrics and progress updates. - Moved cold-cache full serialization for
library.getAllto a background thread. The SDK waits forlibrary:getAllResultand correlates it byrequestIdso large-library queries do not block the UI. - Fixed the window restoration path after hiding to tray, including WebView surface recovery for
window.focus/ hidden restore, reducing blank surfaces after minimize, tray hide, or Alt+Tab restoration. - Fixed corrupt base64 for the taskbar-thumbnail pause icon and corrected HICON ownership, preventing malformed playback buttons and explorer.exe crashes.
- Updated the SDK package to
1.7.0, including the new Taskbar & Tray types and event declarations. - Added a Taskbar & Tray API page to VitePress and synchronized Cursor, high-frequency Playback events, and related examples.
v1.6.1 (2026-05-20)
- Added the
cursor.*namespace:cursor.setHidden(hidden)/cursor.isHidden()explicitly control client-area cursor visibility, addressing unreliable CSScursor: nonebehavior under Visual Hosting. - Added per-window
cursor:hiddenChangedevents. - Added the
insecureTlsparameter tofb.http.*behind two gates: the globalAllow self-signed / invalid TLS certificatessetting must be ON and the request must specifyinsecureTls: true. This allows explicitly authorized access to self-signed intranet services such as Plex / Jellyfin / Lidarr. - Added
responseType: 'arraybuffer' | 'binary'tofb.http.*; the body is base64-decoded to anArrayBuffer, so binary artwork and fonts no longer fail strict UTF-8 validation. - Updated the VitePress cursor.md / http.md / events.md documentation for these changes.
v1.6.0 (2026-05-11)
- Removed
durationfromplaylist.getAllto avoid reading every track solely to calculate duration;playlist.getActive/playlist.getPlayingstill return it. - Changed
http.get/http.post/http.headto asynchronous by default. Passasync: falseexplicitly for a synchronous call.
v1.1.17 (2026-02-06)
- Added full multi-window support.
- Added
window.createPopup/closePopup/closeAllPopups/getAllWindows. - Added
window.sendMessage/window.broadcastfor inter-window messaging. - Added asynchronous close, frameless windows, and transparent backgrounds.