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.
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 ComponentThe 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)
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.
query (string): The user's search input.limit (number): Max items to return (usually
10-50).
Promise<{ tracks: Track[], total: number }>
getTrackStreamUrl(id, quality)
Resolves a track ID to a playable URL.
id (string/number): The ID returned from
searchTracks.quality (string): Requested quality ('LOSSLESS'
| 'HIGH').
Promise<{ streamUrl: string, track: Track }>
getAlbum(id) Optional
Fetches album details and tracklist.
Promise<{ album: AlbumInfo, tracks: Track[] }>
8SPINE