Untitled
1) Architecture overview
- Core library:
extendify(a shared library/DLL built fromsrc/**) is the actual mod engine loaded into Spotify’s CEF process. - Bootstrap/injector:
extendify_wrapper(Windows: DLL; Linux: SO) is a tiny loader injected into Spotify’s process. It locates and loads theextendifyshared library using the environment variableEXTENDIFY_LIB_PATH. - Hooking layer: A small cross-platform abstraction over function hooking:
- Windows: Microsoft Detours (
detours) for hot-patching function entries. - Linux: Dobby (
dobby_static) for inline hooks.
- Windows: Microsoft Detours (
- Browser engine (CEF): The project links to Spotify’s CEF build (
libcefandcef_dll_wrapper) so it can hook into CEF lifecycle and the V8 engine to expose a native API inside Spotify’s web context. - Exposed JS API: When a Spotify V8 context is created (detected via a CEF hook), the library injects a global
ExtendifyNativeobject with submodules:settings— persistent JSON configquickCss— live-editable CSS file with change notificationsthemes— user theme management (directory, upload, delete, metadata, change notifications)
- Supporting services:
- File watching (Windows IOCP-based in
fs::Watcher) - File dialogs (
FilePicker) on both platforms (Windows Shell dialogs / GTK on Linux) - Logging (
spdlog) to rotating logs under the extendify config dir - Safe JSON helpers, V8 context utilities, and message boxes (Windows)
- File watching (Windows IOCP-based in
Result: Extendify loads early in Spotify, hooks CEF init and V8 function creation to inject a JS bridge, and provides file-based customization (settings, CSS, themes) with live updates.
2) Build and dependencies
- Top-level: CMakeLists.txt builds the shared library target
extendifyand adds wrapper for the injector. - External deps resolved by
cmake/Find*.cmakemodules:libcefandcef_dll_wrapperfetched from Spotify’s CEF build. Platform-conditional setup (Windows vs Linux, x64 vs arm64).spdlogfetched from spdlog.detoursfrom detours (Windows),dobbyfrom dobby (Linux).
- Compiler flags: C++23, warnings, platform-specific extras; debug symbols when supported; some warning suppressions.
- Build artifacts: Copies
extendify_wrapperinto build output. On Windows, also expects and copies Detours’setdll.exetool into build (used by scripts).
3) Injection flow
- Wrapper library: win32.c and linux.c
- Windows
DllMainruns onDLL_PROCESS_ATTACH, readsEXTENDIFY_LIB_PATH, checks the current command line containsSpotify.exe, then callsLoadLibraryA(path)to load the realextendifyDLL.- Optionally attaches to the parent console for stdout/stderr visibility.
- Linux
__attribute__((constructor))readsEXTENDIFY_LIB_PATH, reads/proc/self/cmdline, checks for a marker (.spotify-wrapped), thendlopen(path, RTLD_NOW)the realextendifySO.
- Windows
- How the wrapper gets into Spotify:
- Windows: The repo includes a script injectWrapper.ps1 that uses Detours’
setdll.exeto add/remove the wrapper DLL from theSpotify.exeimage (in%APPDATA%\Spotify\Spotify.exe). Tasks exist to run this easily from VS Code. - Linux: The Nix-related scripts and the
.spotify-wrappedmarker imply a platform-specific wrapper packaging/launch strategy (not fully shown here) that starts Spotify via a wrapped executable name.
- Windows: The repo includes a script injectWrapper.ps1 that uses Detours’
Environment variable used: EXTENDIFY_LIB_PATH must point to the built extendify library (DLL/SO) so the wrapper can load it.
4) Hooking and initialization sequence
- Entry and lifecycle: main.cpp defines
DllMain(Windows) or ELF constructor/destructor (Linux). On process attach:- Calls
Extendify::api::entrypoint::init()(see below).
- Calls
- Entrypoint hook: entrypoint.cpp
- Hooks
cef_initializeviahook::hookFunction:- Ensures
remote_debugging_portis set (defaults to9229) so DevTools can attach to CEF. - Calls original
cef_initialize. - Starts the file watcher thread
fs::Watcher::get()->init().
- Ensures
- Also initializes the JS injection hook (below).
- Hooks
- JavaScript injection hook: jsInjection.cpp
- Hooks
cef_v8_value_create_function. When CEF creates the V8 function namedsendCosmosRequest(a stable signal the main window context is up), it callsapi::inject(CefV8Context::GetEnteredContext()). - This places
ExtendifyNativeonwindowand setsEXTENDIFY_NATIVE_AVAILABLE = true.
- Hooks
- Hook implementation details:
- Windows: hook.cpp uses Detours
DetourTransactionBegin/UpdateThread/Attach/Commitfor attach andDetachfor cleanup. Errors logged with detailed messages. - Linux: Uses Dobby’s
DobbyHook/DobbyDestroy.
- Windows: hook.cpp uses Detours
5) The injected JavaScript API
api.cpp constructs ExtendifyNative inside the current V8 context:
-
ExtendifyNative.settings(src/api/settings.*)get(): Reads JSON settings from%APPDATA%\extendify\config.json(Windows) or$XDG_CONFIG_HOME/extendify/config.json(Linux; falls back to$HOME/.config/extendify/config.json) viapath::getConfigFilePath(true)andfs::readFile.set(obj): Stringifies the supplied JS object withwindow.JSON.stringify(viaapi::util::json) and writes it to the settings file.getSettingsDir(): Returns the path to the directory containing the settings file.- A default settings blob is used to bootstrap missing files:
{ "plugins": {}, "theme": { "files": [], "colors": [] } } - Strict V8 type validation enforced through
APIUsage(src/api/util/APIUsage.*).
-
ExtendifyNative.quickCss(src/api/quickCss.*)get(): Read the current contents ofquickCss.cssunder the extendify config dir.set(string): Write CSS to the same file (file watcher will notify listeners).addChangeListener(fn): Register a callback that fires wheneverquickCss.csschanges on disk. Internally:- The first time the API is built, it registers a watch on the file using
fs::Watcher. - On any change, it posts to the CEF renderer thread and calls all registered callbacks with the new CSS string in the saved
quickCssV8 context. - If listeners are added from multiple V8 contexts, it invalidates previous ones to avoid cross-context issues.
- The first time the API is built, it registers a watch on the file using
openFile()/openEditor()open the quick CSS file in the user’s default editor (Windows viaShellExecuteW, Linux viaxdg-open).
-
ExtendifyNative.themes(src/api/themes.*)uploadTheme() -> Promise: Shows a file picker dialog so the user can select a theme file (*.css,*.theme.css). Copy is performed into the themes folder. Rejects if a duplicate filename exists.deleteTheme(absolutePath) -> Promise<bool>: Displays a confirmationMessageBox(Windows), validates the path is inside the themes dir, and deletes the file if confirmed.addChangeListener(fn): Registers a callback that fires when files are added/removed/modified inside the themes folder.getThemesDir(): Absolute path of the themes directory.getThemes() -> UserTheme[]: Returns a JSON array of currently discovered themes.getThemeData(fileName) -> UserTheme|undefined: Returns metadata for a theme by its relative filename inside the themes dir.- Theme metadata parsing:
- Reads each theme’s CSS and parses an optional header comment block with BetterDiscord/Vencord-style
@fields(e.g.,@name,@author, …). - Produces a
UserThemeobject{ fileName, name, content, author, description, version?, license?, source?, website?, invite? }.
- Reads each theme’s CSS and parses an optional header comment block with BetterDiscord/Vencord-style
ThemeCache:- Holds loaded themes in an
std::unordered_set<std::shared_ptr<UserTheme>>guarded bystd::shared_mutex. - Initialized by scanning the themes directory on first use.
- A recursive directory watch is established; on changes, the cache is updated and listeners are notified on the renderer thread.
- Holds loaded themes in an
Common API infrastructure:
APIUsageandAPIFunctiondefine and validate function signatures and render informative usage strings if incorrect types are passed.CBHandlerimplements aCefV8Handlerwrapper to wire native C++ lambdas to JS-callable functions.ScopedV8Contextsafely enters/exits a V8 context, with trace logging.jsonhelpers callwindow.JSON.parse/stringifyinside the current V8 context and throw with logged diagnostics when exceptions occur.
6) File system and UI services
fs::Watcher(Windows) —src/fs/Watcher.*- Architecture:
- Per-directory watcher objects (
Dir) based onReadDirectoryChangesWwith OVERLAPPED I/O. - A global IO Completion Port receives change events; a secondary thread processes and dispatches them to user callbacks.
- Supports watching specific files (mapped to IDs) and whole directories (recursive callbacks), and fan-out to all registered watchers.
- Per-directory watcher objects (
- Events handled: created, removed, modified, rename old/new name, normalized into
Watcher::Reason. - Public API:
addFile(path, callback)->expected<int, string>IDaddDir(path, callback)->expected<int, string>IDremoveFile(id)init()must be called after CEF initialize (handled in entrypoint hook).
- Architecture:
fs::FilePicker—src/fs/FilePicker.*- Windows:
- Uses modern Shell dialogs (
IFileOpenDialog/IFileSaveDialog), with custom file filters and multi-select when appropriate. - Adds a small COM event sink (
IFileDialogEvents) and internal RAII wrappers for COM pointers.
- Uses modern Shell dialogs (
- Linux:
- Uses GTK (
Gtk::FileChooserDialog) with an event loop runner (GDialogRunner.c) that runs a GtkDialog with a suppliedGMainContext.
- Uses GTK (
- Exposes:
promise(context, callback)returning a JSPromiseresolved or rejected inside the given V8 context.- Convenience constructors for
pickOne,pickMany,pickFolder,pickSaveFile.
- Windows:
fs::fs—src/fs/fs.*readFile,writeFilewith error logging.openPathopens viaShellExecuteW(Windows) or launchesxdg-openfrom the CEF process launcher thread (Linux).- Windows-specific
ensureFilesCanOpenInVscode()clearsELECTRON_RUN_AS_NODEto avoid VS Code opening quirks.
7) Logging
Extendify::log::Loggerwrapsspdlog::logger:- Default sinks: rotating file under
path::getLogDir()/native.logand colored stderr. - Global registry of all logger instances with
Logger::setLevelForAll. - Created per-subsystem, e.g.,
{"Extendify","api","quickCss"}to get hierarchical names likeExtendify/api/quickCss.
- Default sinks: rotating file under
8) Paths and persistence
src/path/path.*- Base config directory:
- Linux:
$XDG_CONFIG_HOME/extendifyor$HOME/.config/extendify - Windows:
%APPDATA%\extendify
- Linux:
- Files:
config.json— settings, ensuring default content on first accessquickCss.css— the Quick CSS filethemes/— themes directorylogs/— logs directory
- Helpers
ensureDir,ensureFileto create directories/files and bootstrap content.
- Base config directory:
9) Misc utilities
util/string.*— splits, trims, regex replace, Win32 wide/narrow conversions.util/TaskCBHandler.*— aCefTaskthat runs an arbitrary function on a CEF thread (used to marshal back to renderer).util/MessageBox.*— WindowsMessageBoxWwrapper supporting:launch(callback)for callback-based completionpromise(context, callback)to produce a JSPromiseresolved with aV8Valuebased on the callback result
util/util.*— small helpers: process-kind detection,intoconverter,mergeResult.
10) What this enables in Spotify
Grounded in the code paths above, Extendify enables:
-
Live CSS customization:
- A dedicated
quickCss.cssthat you can open in your editor and edit while Spotify is running. - Changes are picked up automatically by
fs::Watcherand dispatched to registered JS listeners viaExtendifyNative.quickCss.addChangeListener. - From the JS side (e.g., a frontend mod or a script), you can read/write the CSS and respond to updates.
- A dedicated
-
Theme management:
- A designated
themes/directory for CSS theme files. - Upload themes through a native file picker (
ExtendifyNative.themes.uploadTheme()), which copies files into the themes directory. - Delete themes (with confirmation) and receive change events when theme files are added/removed/modified.
- Enumerate and inspect themes via
getThemes()/getThemeData(), including parsed metadata from a BD/Vencord-style header block.
- A designated
-
Persistent settings:
- A JSON-based config object (
config.json) accessible throughExtendifyNative.settings.get()andset(obj), with a stable location per platform. - Ability for your UI code to store preferences (selected theme, toggles, plugin configuration, color presets, etc.).
- A JSON-based config object (
-
DevTools access:
- Ensured
remote_debugging_port(default 9229) incef_initialize, making it easier to connect Chrome DevTools to Spotify’s CEF instance for debugging.
- Ensured
-
Native UX primitives:
- Use the OS file picker to let users choose files/folders within the mod UI.
- Show simple confirm/ok/cancel message boxes (Windows implementation in place).
Notably, there’s no direct hooking into Spotify audio/network logic here; the primary surface is customizing the UI layer (CEF + DOM/CSS/JS), with robust persistence and live reload.
11) Developer workflow helpers
-
Tasks (VS Code):
cmake: Configureandcmake: Buildto set up and compile everything.shell: generate launch.jsonto point the debugger at your local%APPDATA%\Spotify\Spotify.exe.shell: inject wrapperrunsscripts/injectWrapper.ps1to add/remove the wrapper DLL on the Spotify executable.
-
Windows build notes: See
building.w32.mdfor required tooling (LLVM/clang-cl, CMake, Ninja, Windows SDK, VS build tools). The build also expects Detours’setdll.exeinbuild/(copied by CMake).
Summary
-
Architecture: A wrapper injects the core native library into Spotify’s process. The core hooks CEF init and V8 function creation to inject a global
ExtendifyNativeJavaScript API. That API exposes persistent settings, live-editable CSS, and theme management, all backed by a Windows-optimized file watcher and native dialogs. -
Technical details: Hooks (
Detours/Dobby), CEF/V8 APIs,fs::Watcher(IOCP + ReadDirectoryChangesW),FilePicker(COM shell/GTK), andspdloglogging. Paths under a per-userextendifyconfig root. -
What it enables: UI customization for Spotify through CSS and themes, persistent configuration, native file dialogs, message boxes, and CEF DevTools access via a fixed remote debugging port. - Strict V8 type validation enforced through
APIUsage(src/api/util/APIUsage.*). -
ExtendifyNative.quickCss(src/api/quickCss.*)get(): Read the current contents ofquickCss.cssunder the extendify config dir.set(string): Write CSS to the same file (file watcher will notify listeners).addChangeListener(fn): Register a callback that fires wheneverquickCss.csschanges on disk. Internally:- The first time the API is built, it registers a watch on the file using
fs::Watcher. - On any change, it posts to the CEF renderer thread and calls all registered callbacks with the new CSS string in the saved
quickCssV8 context. - If listeners are added from multiple V8 contexts, it invalidates previous ones to avoid cross-context issues.
- The first time the API is built, it registers a watch on the file using
openFile()/openEditor()open the quick CSS file in the user’s default editor (Windows viaShellExecuteW, Linux viaxdg-open).
-
ExtendifyNative.themes(src/api/themes.*)uploadTheme() -> Promise: Shows a file picker dialog so the user can select a theme file (*.css,*.theme.css). Copy is performed into the themes folder. Rejects if a duplicate filename exists.deleteTheme(absolutePath) -> Promise<bool>: Displays a confirmationMessageBox(Windows), validates the path is inside the themes dir, and deletes the file if confirmed.addChangeListener(fn): Registers a callback that fires when files are added/removed/modified inside the themes folder.getThemesDir(): Absolute path of the themes directory.getThemes() -> UserTheme[]: Returns a JSON array of currently discovered themes.getThemeData(fileName) -> UserTheme|undefined: Returns metadata for a theme by its relative filename inside the themes dir.- Theme metadata parsing:
- Reads each theme’s CSS and parses an optional header comment block with BetterDiscord/Vencord-style
@fields(e.g.,@name,@author, …). - Produces a
UserThemeobject{ fileName, name, content, author, description, version?, license?, source?, website?, invite? }.
- Reads each theme’s CSS and parses an optional header comment block with BetterDiscord/Vencord-style
ThemeCache:- Holds loaded themes in an
std::unordered_set<std::shared_ptr<UserTheme>>guarded bystd::shared_mutex. - Initialized by scanning the themes directory on first use.
- A recursive directory watch is established; on changes, the cache is updated and listeners are notified on the renderer thread.
- Holds loaded themes in an
Common API infrastructure:
APIUsageandAPIFunctiondefine and validate function signatures and render informative usage strings if incorrect types are passed.CBHandlerimplements aCefV8Handlerwrapper to wire native C++ lambdas to JS-callable functions.ScopedV8Contextsafely enters/exits a V8 context, with trace logging.jsonhelpers callwindow.JSON.parse/stringifyinside the current V8 context and throw with logged diagnostics when exceptions occur.