Soggfy (SpotifyOggDumper)

repository·master·Indexed 23 days ago

https://github.com/rafiuth/soggfy

A music downloader mod for the Windows Spotify client that intercepts unencrypted OGG data during playback to create exact copies of tracks, including metadata, lyrics, and canvas. It consists of an Injector, a Core DLL (SpotifyOggDumper) for audio hooks and file management, and a Sprinkles UI layer for metadata and configuration.

Tokens
4.5K
Snippets
6
Records
21
Agent score
81%

What's inside Soggfy

  1. Understand the Soggfy architecture

    master

    Soggfy is composed of three distinct layers:

    • Injector: Handles the launching of Spotify and the loading of the Core DLL into the Spotify process.
    • Core DLL (SpotifyOggDumper): Manages audio/player hooks, state management, and file management.
    • Sprinkles: The UI integration layer. It is a single JavaScript bundle that is injected into the main CEF (Chromium Embedded Framework) window by the Core DLL and acts as a track metadata provider.
  2. Build Soggfy locally

    master

    To build Soggfy from source, ensure you have Visual Studio 2022+ (or Build Tools for VS) and Node.js 16+ installed.

    1. Open a "Native Tools CMD for VS".
    2. Navigate to the repository directory.
    3. Execute the build sequence to fetch dependencies, compile the C++ Core DLL, and build the Sprinkles UI bundle.

    Note: The fetch_external_deps.bat script requires curl and 7z to be available in your PATH to download CEF headers. If the script fails, you can manually download the .tar file (referenced in the .bat file) and extract it into the external/cef_bin/ folder.

    set PATH=C:\Program Files\7-Zip;%PATH%
    fetch_external_deps.bat
    
    mscbuild /p:Configuration=Release
    
    cd Sprinkles
    npm run build
    copy dist\bundle.js ..\build\Release\Soggfy.js /y
  3. Debug the Core DLL

    master

    Debugging the Core DLL is performed by attaching the Visual Studio debugger to the main Spotify process after the DLL has been injected.

    Since there are no anti-debugging protections, you can attach directly. To avoid restarting Spotify during development, you can "uninject" the DLL by typing u in the console, then rebuild and reinject the DLL.

  4. Debug Sprinkles (UI) via Remote Debugging

    master

    Because built-in Spotify DevTools are disabled, the most reliable way to develop and debug the Sprinkles JS bundle is via remote debugging.

    1. Launch Spotify with the remote debugging port enabled: Spotify.exe --remote-debugging-port=9222.
    2. Open a Chrome-based browser and navigate to chrome://inspect to access the remote DevTools.
    Spotify.exe --remote-debugging-port=9222
  5. Install Soggfy for Windows Spotify client

    master

    To install Soggfy, follow these steps:

    1. Download and extract the .zip package from the latest release.
    2. Double-click the Install.cmd file. This executes the Install.ps1 script with Execution Policy Bypass. Wait for the process to complete.
    3. Open Spotify and play the songs you wish to download.

    Troubleshooting:

    • Anti-virus: You may need to disable or whitelist Soggfy in your anti-virus software.
    • Missing DLLs: If the Spotify client crashes due to missing DLLs, install the MSVC Redistributable package.
  6. Manually install Soggfy

    master

    If the automated Install.cmd script fails, perform a manual installation:

    1. Download and install the correct Spotify client version (the specific link is located inside the Install.ps1 script).
    2. Copy and rename SpotifyOggDumper.dll to %appdata%/Spotify/dpapi.dll.
    3. Copy SoggfyUIC.js to %appdata%/Spotify/SoggfyUIC.js.
    4. Download and extract FFmpeg binaries to %localappdata%/Soggfy/ffmpeg/ffmpeg.exe (or add them to your system %PATH%).

    Alternative Injection: You can use Injector.exe to launch or inject Soggfy into an existing Spotify instance. For a portable installation, copy Spotify binaries from %appdata%/Spotify/ to Soggfy/Spotify/.

  7. Use Soggfy to download music

    master

    Once installed, Soggfy operates by intercepting Spotify's OGG parser during playback.

    Usage Rules:

    • Playback: Songs are only downloaded if played from start to finish without seeking (pausing is allowed).
    • Accessing Files: Tracks are saved in the Music folder by default. Hovering over the check mark on an individual track in the Spotify UI will show a popup to open the containing folder.
    • Settings: Access the settings panel by hovering next to the download button in the navigation bar.

    Quality and Limitations:

    • Audio Quality: Depends on your Spotify plan. Free accounts get ~160Kb/s; Premium accounts get ~320Kb/s. To ensure 320Kb/s, set Spotify streaming quality to "Very high".
    • Podcasts: Support is limited and typically only works with audio-only OGG podcasts.
    • Warning: Using this mod breaks Spotify's Guidelines and may result in an account ban. It is recommended to use an alternative account.
  8. How Soggfy hooks Spotify audio data

    master

    Soggfy works by injecting a DLL into the Spotify process and hooking the DecodeAudioData function. This function is responsible for taking compressed audio packets (OGG/Vorbis, AAC, or MP3) and decoding them into raw PCM samples.

    By intercepting this call, Soggfy can:

    1. Extract encoded audio: It calculates the difference in size in the param_4 buffer to capture the raw encoded bytes.
    2. Identify the song: It uses a pointer traversal technique to find the PlayerState from the stack, allowing it to retrieve a unique 16-byte playback_id. This ID is used to link the audio data to a specific playback session.
    3. Handle playback speed: If the playback speed is greater than 1.0, Soggfy modifies the param_3 (PCM sample) buffer to skip samples, effectively matching the requested playback speed.

    All extracted audio data is then passed to the StateManager via ReceiveAudioData(playbackId, data, size).

  9. Identify the DecodeAudioData target function

    master

    The DecodeAudioData function is the primary target for Soggfy's hooks. It is difficult to find via static analysis because it lacks distinct search features.

    To find it manually:

    1. Locate the main OGG parser function by searching for the magic bytes OggS (or the value 4).
    2. Use a debugger (like x64dbg) to inspect the call stack from the OGG parser to find the dispatcher function.
    3. The function signature is: int DecodeAudioData(void* ecx, void* edx, int param_2, AudioSpan<float>* param_3, AudioSpan<char>* param_4, int param_5)

    As of Spotify v1.2.25, the hook uses the following pattern signature: 55 8B EC 51 56 8B 75 0C 8D 55 0C 57 FF 75 14 8B 7D 10 8B 46 04 52 8D 55 FC 89 45 FC FF 37 8B 47 04 52 FF 36 89 45 0C 8B 01 FF 75 08 FF 50 04 8B 06 8B 4D FC 29 4E 04 8D 14 88 8B 45 08 89 16 8B 17 8B 4F 04 03 55 0C 2B 4D 0C 89 17 89 4F 04 5F 5E C9 C2 10 00

  10. Configure Soggfy data and log directories

    master

    Soggfy determines its working directory using a portable configuration heuristic:

    1. Primary Data Directory: %LocalAppData%\Soggfy.
    2. Portable Mode: If a config.json exists in the same directory as the Soggfy DLL, or if the DLL is injected without a persistent local app data folder, Soggfy will use the DLL's directory as the data directory.

    Logs:

    • Logs are written to log.txt within the determined data directory.
    • In non-debug builds, if a file named _debug.txt exists in the data directory, Soggfy enables TRACE level logging. Otherwise, it defaults to DEBUG.
    • While running in a console environment, you can cycle log levels by pressing l (cycling through INFO, DEBUG, TRACE) and exit by pressing u.
  11. Configure Soggfy via the config object

    master

    The config object controls the behavior of the downloader, including playback speed, metadata embedding, and file organization.

    Key configuration sections include:

    • Playback & Download Control: playbackSpeed, downloaderEnabled, skipDownloadedTracks, and skipIgnoredTracks.
    • Metadata & Media: embedLyrics, saveLyrics, embedCoverArt, saveCoverArt, and saveCanvas.
    • Output Format: outputFormat allows specifying FFmpeg args (e.g., "-c copy") and the file extension ext.
    • Path Templates: savePaths defines how files are organized using placeholders like {artist_name}, {album_name}, {track_num}, and {track_name}. It also includes invalidCharRepl (e.g., "unicode") for handling illegal filename characters.
    • Filtering: ignorelist is an object used to define Resource URIs that should be skipped by the downloader. blockAds enables/disables ad blocking.
    let config = {
        playbackSpeed: 1.0,
        downloaderEnabled: true,
        skipDownloadedTracks: false,
        skipIgnoredTracks: false,
        embedLyrics: true,
        saveLyrics: true,
        embedCoverArt: true,
        saveCoverArt: true,
        saveCanvas: false,
        outputFormat: {
            args: "-c copy",
            ext: ""
        },
        savePaths: {
            basePath: "",
            track: "{artist_name}/{album_name}{multi_disc_path}/{track_num}. {track_name}.ogg",
            episode: "Podcasts/{artist_name}/{album_name}/{release_date} - {track_name}.ogg",
            canvas: "{artist_name}/{album_name}{multi_disc_path}/Canvas/{track_num}. {track_name}.mp4",
            invalidCharRepl: "unicode",
        },
        ignorelist: {
        },
        blockAds: true
    };
  12. Filter tracks using isTrackIgnored()

    master

    The isTrackIgnored(track) function determines if a track should be skipped based on the ignorelist defined in the configuration.

    It checks the following identifiers against the ignorelist keys:

    1. The track's own URI (track.uri).
    2. The album's URI (track.album?.uri or track.albumUri).
    3. The metadata context URI (track.metadata?.context_uri or track.contextUri).
    4. The URIs of all associated artists (track.artists).

    If any of these URIs exist as a key in config.ignorelist, the function returns true.

    export function isTrackIgnored(track) {
        let tieUris = [
            track.uri,
            track.album?.uri ?? track.albumUri,
            track.metadata?.context_uri ?? track.contextUri,
            ...(track.artists ?? [])
        ];
        return tieUris.some(res => config.ignorelist[res?.uri ?? res]);
    }