Untitled

 avatar
unknown
markdown
10 months ago
18 kB
15
Indexable

1) Architecture overview

  • Core library: extendify (a shared library/DLL built from src/**) 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 the extendify shared library using the environment variable EXTENDIFY_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.
  • Browser engine (CEF): The project links to Spotify’s CEF build (libcef and cef_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 ExtendifyNative object with submodules:
    • settings — persistent JSON config
    • quickCss — live-editable CSS file with change notifications
    • themes — 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)

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 extendify and adds wrapper for the injector.
  • External deps resolved by cmake/Find*.cmake modules:
    • libcef and cef_dll_wrapper fetched from Spotify’s CEF build. Platform-conditional setup (Windows vs Linux, x64 vs arm64).
    • spdlog fetched from spdlog.
    • detours from detours (Windows), dobby from dobby (Linux).
  • Compiler flags: C++23, warnings, platform-specific extras; debug symbols when supported; some warning suppressions.
  • Build artifacts: Copies extendify_wrapper into build output. On Windows, also expects and copies Detours’ setdll.exe tool into build (used by scripts).

3) Injection flow

  • Wrapper library: win32.c and linux.c
    • Windows
      • DllMain runs on DLL_PROCESS_ATTACH, reads EXTENDIFY_LIB_PATH, checks the current command line contains Spotify.exe, then calls LoadLibraryA(path) to load the real extendify DLL.
      • Optionally attaches to the parent console for stdout/stderr visibility.
    • Linux
      • __attribute__((constructor)) reads EXTENDIFY_LIB_PATH, reads /proc/self/cmdline, checks for a marker (.spotify-wrapped), then dlopen(path, RTLD_NOW) the real extendify SO.
  • How the wrapper gets into Spotify:
    • Windows: The repo includes a script injectWrapper.ps1 that uses Detours’ setdll.exe to add/remove the wrapper DLL from the Spotify.exe image (in %APPDATA%\Spotify\Spotify.exe). Tasks exist to run this easily from VS Code.
    • Linux: The Nix-related scripts and the .spotify-wrapped marker imply a platform-specific wrapper packaging/launch strategy (not fully shown here) that starts Spotify via a wrapped executable name.

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).
  • Entrypoint hook: entrypoint.cpp
    • Hooks cef_initialize via hook::hookFunction:
      • Ensures remote_debugging_port is set (defaults to 9229) so DevTools can attach to CEF.
      • Calls original cef_initialize.
      • Starts the file watcher thread fs::Watcher::get()->init().
    • Also initializes the JS injection hook (below).
  • JavaScript injection hook: jsInjection.cpp
    • Hooks cef_v8_value_create_function. When CEF creates the V8 function named sendCosmosRequest (a stable signal the main window context is up), it calls api::inject(CefV8Context::GetEnteredContext()).
    • This places ExtendifyNative on window and sets EXTENDIFY_NATIVE_AVAILABLE = true.
  • Hook implementation details:
    • Windows: hook.cpp uses Detours DetourTransactionBegin/UpdateThread/Attach/Commit for attach and Detach for cleanup. Errors logged with detailed messages.
    • Linux: Uses Dobby’s DobbyHook/DobbyDestroy.

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) via path::getConfigFilePath(true) and fs::readFile.
    • set(obj): Stringifies the supplied JS object with window.JSON.stringify (via api::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 of quickCss.css under the extendify config dir.
    • set(string): Write CSS to the same file (file watcher will notify listeners).
    • addChangeListener(fn): Register a callback that fires whenever quickCss.css changes 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 quickCss V8 context.
      • If listeners are added from multiple V8 contexts, it invalidates previous ones to avoid cross-context issues.
    • openFile() / openEditor() open the quick CSS file in the user’s default editor (Windows via ShellExecuteW, Linux via xdg-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 confirmation MessageBox (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 UserTheme object { fileName, name, content, author, description, version?, license?, source?, website?, invite? }.
    • ThemeCache:
      • Holds loaded themes in an std::unordered_set<std::shared_ptr<UserTheme>> guarded by std::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.

Common API infrastructure:

  • APIUsage and APIFunction define and validate function signatures and render informative usage strings if incorrect types are passed.
  • CBHandler implements a CefV8Handler wrapper to wire native C++ lambdas to JS-callable functions.
  • ScopedV8Context safely enters/exits a V8 context, with trace logging.
  • json helpers call window.JSON.parse/stringify inside 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 on ReadDirectoryChangesW with 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.
    • Events handled: created, removed, modified, rename old/new name, normalized into Watcher::Reason.
    • Public API:
      • addFile(path, callback) -> expected<int, string> ID
      • addDir(path, callback) -> expected<int, string> ID
      • removeFile(id)
      • init() must be called after CEF initialize (handled in entrypoint hook).
  • fs::FilePickersrc/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.
    • Linux:
      • Uses GTK (Gtk::FileChooserDialog) with an event loop runner (GDialogRunner.c) that runs a GtkDialog with a supplied GMainContext.
    • Exposes:
      • promise(context, callback) returning a JS Promise resolved or rejected inside the given V8 context.
      • Convenience constructors for pickOne, pickMany, pickFolder, pickSaveFile.
  • fs::fssrc/fs/fs.*
    • readFile, writeFile with error logging.
    • openPath opens via ShellExecuteW (Windows) or launches xdg-open from the CEF process launcher thread (Linux).
    • Windows-specific ensureFilesCanOpenInVscode() clears ELECTRON_RUN_AS_NODE to avoid VS Code opening quirks.

7) Logging

  • Extendify::log::Logger wraps spdlog::logger:
    • Default sinks: rotating file under path::getLogDir()/native.log and colored stderr.
    • Global registry of all logger instances with Logger::setLevelForAll.
    • Created per-subsystem, e.g., {"Extendify","api","quickCss"} to get hierarchical names like Extendify/api/quickCss.

8) Paths and persistence

  • src/path/path.*
    • Base config directory:
      • Linux: $XDG_CONFIG_HOME/extendify or $HOME/.config/extendify
      • Windows: %APPDATA%\extendify
    • Files:
      • config.json — settings, ensuring default content on first access
      • quickCss.css — the Quick CSS file
      • themes/ — themes directory
      • logs/ — logs directory
    • Helpers ensureDir, ensureFile to create directories/files and bootstrap content.

9) Misc utilities

  • util/string.* — splits, trims, regex replace, Win32 wide/narrow conversions.
  • util/TaskCBHandler.* — a CefTask that runs an arbitrary function on a CEF thread (used to marshal back to renderer).
  • util/MessageBox.* — Windows MessageBoxW wrapper supporting:
    • launch(callback) for callback-based completion
    • promise(context, callback) to produce a JS Promise resolved with a V8Value based on the callback result
  • util/util.* — small helpers: process-kind detection, into converter, mergeResult.

10) What this enables in Spotify

Grounded in the code paths above, Extendify enables:

  • Live CSS customization:

    • A dedicated quickCss.css that you can open in your editor and edit while Spotify is running.
    • Changes are picked up automatically by fs::Watcher and dispatched to registered JS listeners via ExtendifyNative.quickCss.addChangeListener.
    • From the JS side (e.g., a frontend mod or a script), you can read/write the CSS and respond to updates.
  • 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.
  • Persistent settings:

    • A JSON-based config object (config.json) accessible through ExtendifyNative.settings.get() and set(obj), with a stable location per platform.
    • Ability for your UI code to store preferences (selected theme, toggles, plugin configuration, color presets, etc.).
  • DevTools access:

    • Ensured remote_debugging_port (default 9229) in cef_initialize, making it easier to connect Chrome DevTools to Spotify’s CEF instance for debugging.
  • 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: Configure and cmake: Build to set up and compile everything.
    • shell: generate launch.json to point the debugger at your local %APPDATA%\Spotify\Spotify.exe.
    • shell: inject wrapper runs scripts/injectWrapper.ps1 to add/remove the wrapper DLL on the Spotify executable.
  • Windows build notes: See building.w32.md for required tooling (LLVM/clang-cl, CMake, Ninja, Windows SDK, VS build tools). The build also expects Detours’ setdll.exe in build/ (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 ExtendifyNative JavaScript 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), and spdlog logging. Paths under a per-user extendify config 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 of quickCss.css under the extendify config dir.
    • set(string): Write CSS to the same file (file watcher will notify listeners).
    • addChangeListener(fn): Register a callback that fires whenever quickCss.css changes 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 quickCss V8 context.
      • If listeners are added from multiple V8 contexts, it invalidates previous ones to avoid cross-context issues.
    • openFile() / openEditor() open the quick CSS file in the user’s default editor (Windows via ShellExecuteW, Linux via xdg-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 confirmation MessageBox (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 UserTheme object { fileName, name, content, author, description, version?, license?, source?, website?, invite? }.
    • ThemeCache:
      • Holds loaded themes in an std::unordered_set<std::shared_ptr<UserTheme>> guarded by std::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.

Common API infrastructure:

  • APIUsage and APIFunction define and validate function signatures and render informative usage strings if incorrect types are passed.
  • CBHandler implements a CefV8Handler wrapper to wire native C++ lambdas to JS-callable functions.
  • ScopedV8Context safely enters/exits a V8 context, with trace logging.
  • json helpers call window.JSON.parse/stringify inside the current V8 context and throw with logged diagnostics when exceptions occur.
Leave a Comment