8SPINE 8SPINE Docs

8SPINE Module Engine

A modular, plugin-based architecture for high-fidelity audio streaming. The engine separates core player logic from content sources, allowing for infinite extensibility via community modules.

Overview

8SPINE operates on a "Bring Your Own Source" (BYOS) philosophy. The core application provides the UI, caching, playlist management, and audio pipeline. The Module Engine handles the retrieval of metadata and audio streams.

Key Capabilities

  • Dynamic module loading at runtime via Module Manager.
  • Intelligent stream matching via Stream Helper.
  • Cross-module compatibility (e.g., Spotify Search + External Source).
  • Persistent caching of module results.

The Module Manager

The Module Manager is the orchestrator of the engine. It is responsible for the full lifecycle of a module: installation, loading, and execution. It acts as a bridge between the React Native UI and the raw JavaScript code of the modules.

Services/ModuleManager.js Core Logic

Code Parsing & Injection

Modules are stored as strings. The manager uses a sandboxed evaluation technique to instantiate them. Crucially, it handles different export styles:

  • Export Const Wrapper: It detects export const MODULE = `...` patterns, strips the export keyword, and extracts the inner code block using regex. This allows modules to be bundled as ES6 constants.
  • Direct Return: Standard JS that ends with return { ... }.


loadModule(code) {
    let moduleCode = code;

    // 1. Handle Export Wrappers
    // Often modules are wrapped in `export const NAME = ...` for portability.
    // The manager strips this to get to the raw function body.
    if (hasExportWrapper) {
        moduleCode = extractInnerCode(moduleCode);
    }

    // 2. Instantiate
    // The code is executed as a new Function.
    const createModule = new Function(moduleCode);
    const moduleInstance = createModule();

    // 3. Register
    this.modules.set(moduleInstance.id, moduleInstance);
}

Proxy Pattern

The Manager acts as a proxy. When the app requests searchTracks, the Manager forwards the request to the currently Active Module. This allows hot-swapping sources without restarting the app.

Persistence

Modules and the user's "Active Module" preference are persisted in AsyncStorage. On app launch, the `init()` method rehydrates the engine and automatically re-activates the last used source.

Stream Helper Intelligence

Critical Component

The Stream Helper is the intelligence layer. It connects the "Desire" (a track the user wants to play) with the "Source" (a playable stream URL). It is robust against metadata mismatches between services (e.g., spotify naming vs your source naming conventions).

Normalization

Converts all strings to a "canonical" form. Removes smart quotes, standardizes CJK characters to ASCII (e.g., Full-width brackets to standard brackets), and collapses whitespace to ensure broad compatibility.

Romanization Retry

If a Japanese/Korean track fails to find a stream, the engine automatically Romanizes the query (e.g., "アイドル" -> "Idol") and retries. This handles database differences between regions.

Strict Verification

Search results are filtered strictly. The helper ensures the Artist matches exactly (fuzzy) before checking the title, preventing "Cover" or "Karaoke" versions from playing.

The Logic Flow & Retry Matrix

1. Clean & Analyze

Input track metadata is cleaned. If the track name is purely special characters (e.g., "?????"), the search strategy shifts to "Exact Match" mode to avoid search engine confusion.

2. Primary Search

Queries the Active Module with "${Track} ${Artist}".

3. The Retry Matrix (If Primary Fails)

Retry 1: Romanized Track + Romanized Artist // Helps if source is English-only
Retry 2: Original Track + Romanized Artist // Helps if Artist is known internationally but track is local
Retry 3: Romanized Track + Original Artist // Helps if Track is transliterated but Artist uses Kanji

4. Stream Verification

The engine iterates through search candidates. It calls getTrackStreamUrl on them. The first one to return a valid URL (usually LOSSLESS) wins and is cached.

Tutorial: Create a Module

Creating a module for 8SPINE is simple. You just need to return a JavaScript object with specific methods. Follow these four steps to build your own.

Define Metadata

Start by defining the identity of your module. The ID must be unique.

const MY_MODULE = {
    id: 'my-custom-source',
    name: 'My Custom Source',
    version: '1.0.0',
    labels: ['MP3', 'FAST'], // Tags shown in UI
    
    // ... methods will go here
};

Implement Search

The searchTracks method takes a user query and returns a standardized list of tracks. You MUST map your API's response to the 8SPINE track format.

searchTracks: async (query, limit) => {
    // 1. Fetch data from your API
    const url = `https://my-api.com/search?q=${encodeURIComponent(query)}`;
    const res = await fetch(url);
    const data = await res.json();

    // 2. Map to 8SPINE format
    const tracks = data.results.map(item => ({
        id: item.unique_id,           // Crucial: Used later for streaming
        title: item.track_name,
        artist: item.artist_name,
        album: item.album_name,
        duration: item.length_in_seconds,
        albumCover: item.cover_art_url
    }));

    return { tracks: tracks, total: tracks.length };
+},

Implement Streaming

The getTrackStreamUrl method receives the ID you returned in Step 2. It must return a direct, playable audio URL (MP3, FLAC, M4A).

getTrackStreamUrl: async (trackId, quality) => {
    console.log('Fetching stream for:', trackId);

    // 1. Call your API to get the download/stream link
    const res = await fetch(`https://my-api.com/stream/${trackId}`);
    const json = await res.json();

    // 2. Return the direct URL
    return {
        streamUrl: json.direct_audio_link, 
        track: { 
            id: trackId,
            audioQuality: 'HIGH' 
        }
    };
+},

Final Return

Finally, ensure your file ends by returning the object. This is how the Module Manager loads it.

// ... inside your module file ...

return MY_MODULE; 

Standard Contract Reference

For advanced developers, here is the complete interface definition.


// A module is an object returned by the script
return {
    // -- Metadata --
    id: 'string',      // Unique identifier
    name: 'string',     // Display name in UI
    version: 'string',            // Semantic versioning
    labels: ['string'], // UI Tags

    // -- Core Methods --
    
    /**
     * Search for tracks.
     * @param {string} query - The search string
     * @param {number} limit - Max results
     */
    searchTracks: async (query, limit) => { ... },

    /**
     * Get the direct audio stream URL.
     * @param {string} id - The track ID (from search results)
     * @param {string} quality - 'LOSSLESS', 'HIGH', 'LOW'
     */
    getTrackStreamUrl: async (id, quality) => { ... },

    // -- Optional Methods --
    getAlbum: async (id) => { ... },
    getArtist: async (id) => { ... }
};

API Reference

searchTracks(query, limit)

Searches the module's database for tracks.

Params
query (string): The user's search input.
limit (number): Max items to return (usually 10-50).
Returns
Promise<{ tracks: Track[], total: number }>

getTrackStreamUrl(id, quality)

Resolves a track ID to a playable URL.

Params
id (string/number): The ID returned from searchTracks.
quality (string): Requested quality ('LOSSLESS' | 'HIGH').
Returns
Promise<{ streamUrl: string, track: Track }>

getAlbum(id) Optional

Fetches album details and tracklist.

Returns
Promise<{ album: AlbumInfo, tracks: Track[] }>