Skip to content

Plugin Development

Behdad Kanaani edited this page Jun 9, 2026 · 1 revision

KORAI Plugin Development Documentation

Table of Contents

  1. Introduction
  2. Plugin Structure
  3. Manifest File
  4. Plugin API
  5. Hooks System
  6. Permissions System
  7. Development Tools
  8. Examples
  9. Best Practices
  10. Troubleshooting

Introduction

KORAI Player features a powerful plugin system that allows developers to extend the application's functionality. Plugins can:

  • Interact with audio playback
  • Access track metadata and library
  • Process audio in real-time
  • Add custom UI elements
  • Persist settings and data
  • Listen to playback events

Prerequisites

  • Node.js 18+ installed
  • Basic JavaScript/Node.js knowledge
  • Understanding of CommonJS modules

Plugin Structure

A KORAI plugin is a directory containing at minimum:

my-plugin/
├── manifest.json      # Plugin metadata and configuration
├── index.js          # Main plugin entry point
├── package.json      # Optional: for dependencies
├── ui.css            # Optional: custom styles
├── ui.js             # Optional: UI component script
└── node_modules/     # Optional: dependencies

Manifest File

The manifest.json file defines your plugin's metadata, permissions, and entry point.

Schema

{
  "id": "com.example.my-plugin",
  "name": "My Awesome Plugin",
  "version": "1.0.0",
  "entry": "index.js",
  "description": "What this plugin does",
  "author": "Your Name",
  "license": "MIT",
  "permissions": ["api:logging", "api:storage", "api:events"],
  "hooks": ["onLoad", "onTrackPlay", "onTrackPause"],
  "builtin": false,
  "enabled": true
}

Field Definitions

Field Type Required Description
id string Yes Unique identifier (reverse domain or simple name)
name string Yes Display name shown in Plugin Manager
version string Yes Semantic version (e.g., "1.0.0")
entry string Yes Main JavaScript file path (relative to plugin root)
description string No Brief description of plugin functionality
author string No Creator name
license string No License identifier (e.g., "MIT", "GPL-3.0")
permissions array No Required permissions (see Permissions)
hooks array No Declared hooks for validation
builtin boolean No Whether plugin ships with KORAI (default: false)
enabled boolean No Initial enabled state (default: false)

ID Naming Conventions

  • Recommended: com.yourname.plugin-name (reverse domain)
  • Alternative: yourname/plugin-name
  • Simple: my-plugin

Example IDs:

  • com.behdad.visualizer
  • korai/equalizer-pro
  • track-logger

Plugin API

Your plugin receives a context object containing the API interface.

Plugin Class Template

class MyPlugin {
  constructor(context) {
    this.context = context;
    this.api = context.api;
    this.id = context.id;
  }

  async activate(context) {
    this.api.log(`${this.context.name} activated!`);
    // Initialization code here
  }

  async deactivate(context) {
    this.api.log(`${this.context.name} deactivated`);
    // Cleanup code here
  }
}

module.exports = MyPlugin;

API Reference

api.log(message)

Log a message to the console with plugin prefix.

this.api.log('Hello from my plugin!');
// Output: [my-plugin] Hello from my plugin!

api.notify(message, options)

Show a desktop notification.

this.api.notify('Track changed!', { duration: 3000 });
// options: { duration: number, type: 'info'|'success'|'error' }

api.emit(eventName, data)

Emit custom events that other plugins can listen to.

this.api.emit('my-custom-event', { value: 42 });

api.registerHook(name)

Register a custom hook at runtime (for dynamic hooks).

this.api.registerHook('onCustomAction');

api.requestPermission(permission)

Request additional permissions at runtime. Returns Promise.

const granted = await this.api.requestPermission('network');
if (granted) {
  // Make network request
}

api.storage.get(key)

Retrieve a stored value. Returns Promise.

const settings = await this.api.storage.get('userSettings');

api.storage.set(key, value)

Store a value persistently.

await this.api.storage.set('lastTrack', trackId);

api.fs.read(path)

Read a file within the project (requires project:read permission). Returns Promise<{content: string} | {error: string}>.

const result = await this.api.fs.read('src/config.json');
if (result.content) {
  const config = JSON.parse(result.content);
}

api.fs.write(path, content)

Write a file within the project (requires project:write permission).

await this.api.fs.write('logs/plugin.log', 'Log entry');

Hooks System

Hooks are lifecycle methods that KORAI calls when specific events occur.

Available Hooks

Hook Parameters Description
onLoad() none Called when plugin is loaded
onUnload() none Called when plugin is unloaded
onActivate() none Called when plugin is activated
onDeactivate() none Called when plugin is deactivated
onTrackPlay(track) track object Called when a track starts playing
onTrackPause(track) track object Called when playback pauses
onTrackStop(track) track object Called when playback stops
onTrackSeek(track, position) track, position Called when seeking
onVolumeChange(volume) volume (0-1) Called when volume changes
onQueueChange(queue) queue array Called when playlist queue changes
onLibraryChange(tracks) tracks array Called when library is updated
onAudioProcess(data) audio data buffer Called during audio processing (real-time)
onBpmDetect(bpm) bpm number Called when BPM is detected
onEnergyDetect(energy) energy (0-1) Called when energy level changes

Hook Implementation

class AudioVisualizer {
  constructor(context) {
    this.context = context;
    this.api = context.api;
  }

  async activate() {
    this.api.log('Visualizer ready');
  }

  async onTrackPlay(track) {
    this.api.log(`Now playing: ${track.title} by ${track.artist}`);
    this.startVisualization(track.bpm);
  }

  async onAudioProcess(data) {
    // Process audio samples in real-time
    const average = this.calculateAverage(data);
    this.updateVisualizer(average);
  }

  async onTrackPause(track) {
    this.api.log('Playback paused');
    this.stopVisualization();
  }

  calculateAverage(data) {
    // Custom logic
    return 0.5;
  }

  startVisualization(bpm) {
    // Visualization logic
  }

  stopVisualization() {
    // Cleanup
  }
}

module.exports = AudioVisualizer;

Track Object Structure

When hooks receive a track object, it contains:

{
  id: number,           // Unique track ID
  title: string,        // Track title
  artist: string,       // Artist name
  album: string,        // Album name
  duration: number,     // Duration in seconds
  bpm: number,          // Beats per minute
  energy: number,       // Energy level (0-1)
  genre: string,        // Genre
  filePath: string,     // Full file path
  hasCover: boolean,    // Whether cover art exists
  playCount: number,    // Number of plays
  likeCount: number,    // Number of likes
  isLiked: boolean      // User liked status
}

Permissions System

Plugins must declare required permissions in manifest.json. Users grant permissions during installation.

Permission Types

Permission Description
api:logging Write to console/log files
api:events Emit and listen to events
api:storage Read/write persistent storage
api:notifications Show desktop notifications
api:clipboard Access clipboard
filesystem Read/write file system
network Make network requests
audio Process audio data
preferences Modify user preferences
project:read Read project files
project:write Write project files
project:rw Read and write project files

Example: Requesting Permissions

class NetworkPlugin {
  async activate(context) {
    const hasNetwork = await this.api.requestPermission('network');
    if (!hasNetwork) {
      this.api.log('Network permission denied');
      return;
    }
    
    // Make API call
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    this.processData(data);
  }
}

Development Tools

KORAI provides a CLI tool for plugin development.

Installation

npm install -g korai-plugin
# OR use locally:
npx korai-plugin

Commands

Create a new plugin

korai-plugin create my-plugin

This generates:

  • manifest.json with basic configuration
  • index.js with template code
  • README.md with documentation
  • package.json for npm dependencies

Validate a plugin

korai-plugin validate ./my-plugin

Checks:

  • Manifest schema compliance
  • Required fields presence
  • Entry file existence
  • Version format

Pack a plugin for distribution

korai-plugin pack ./my-plugin

Creates a ZIP file named com.example.my-plugin_v1.0.0.zip ready for installation.

Start development server

korai-plugin dev ./my-plugin [port]

Starts a hot-reload development server (port defaults to 3333).


Examples

Example 1: Simple Track Logger

Logs every played track to storage.

manifest.json

{
  "id": "com.example.track-logger",
  "name": "Track Logger",
  "version": "1.0.0",
  "entry": "index.js",
  "description": "Logs every track played",
  "permissions": ["api:storage", "api:logging"],
  "hooks": ["onTrackPlay"]
}

index.js

class TrackLogger {
  constructor(context) {
    this.api = context.api;
    this.logs = [];
  }

  async activate() {
    // Load previous logs
    const saved = await this.api.storage.get('playLogs');
    if (saved) this.logs = saved;
    this.api.log(`Loaded ${this.logs.length} previous logs`);
  }

  async onTrackPlay(track) {
    const logEntry = {
      trackId: track.id,
      title: track.title,
      artist: track.artist,
      timestamp: Date.now()
    };
    
    this.logs.push(logEntry);
    await this.api.storage.set('playLogs', this.logs);
    
    this.api.log(`Logged: ${track.title}`);
    this.api.notify(`Logged: ${track.title}`, { duration: 2000 });
  }

  async deactivate() {
    await this.api.storage.set('playLogs', this.logs);
    this.api.log('TrackLogger saved and deactivated');
  }
}

module.exports = TrackLogger;

Example 2: BPM Display Plugin

Displays BPM in a custom UI element.

manifest.json

{
  "id": "com.example.bpm-display",
  "name": "BPM Display",
  "version": "1.0.0",
  "entry": "index.js",
  "permissions": ["api:logging", "api:notifications"],
  "hooks": ["onTrackPlay", "onBpmDetect"]
}

index.js

class BPMDisplay {
  constructor(context) {
    this.api = context.api;
    this.currentBpm = 120;
  }

  async activate() {
    this.api.log('BPM Display activated');
    this.createUI();
  }

  createUI() {
    // Find or create container for BPM display
    const container = document.querySelector('.player-center-controls');
    if (!container) return;
    
    // Check if element already exists
    if (document.getElementById('bpm-display')) return;
    
    const bpmDiv = document.createElement('div');
    bpmDiv.id = 'bpm-display';
    bpmDiv.style.cssText = `
      position: absolute;
      bottom: -30px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(0,0,0,0.7);
      padding: 4px 12px;
      border-radius: 20px;
      font-size: 12px;
      font-weight: bold;
      color: #00ffd5;
      font-family: monospace;
    `;
    bpmDiv.textContent = `BPM: ${this.currentBpm}`;
    container.style.position = 'relative';
    container.appendChild(bpmDiv);
    
    this.bpmElement = bpmDiv;
  }

  async onTrackPlay(track) {
    this.currentBpm = track.bpm || 120;
    this.updateDisplay();
    this.api.log(`Track BPM: ${this.currentBpm}`);
  }

  async onBpmDetect(bpm) {
    this.currentBpm = bpm;
    this.updateDisplay();
    
    if (bpm > 150) {
      this.api.notify('🔥 High energy track detected!', { duration: 1500 });
    }
  }

  updateDisplay() {
    if (this.bpmElement) {
      this.bpmElement.textContent = `BPM: ${this.currentBpm}`;
      
      // Change color based on BPM
      if (this.currentBpm > 150) {
        this.bpmElement.style.color = '#ff6b6b';
      } else if (this.currentBpm > 120) {
        this.bpmElement.style.color = '#ffd93d';
      } else {
        this.bpmElement.style.color = '#00ffd5';
      }
    }
  }

  async deactivate() {
    if (this.bpmElement) {
      this.bpmElement.remove();
    }
    this.api.log('BPM Display deactivated');
  }
}

module.exports = BPMDisplay;

Example 3: Audio Effect Plugin

Applies real-time audio effects.

manifest.json

{
  "id": "com.example.reverb-effect",
  "name": "Reverb Effect",
  "version": "1.0.0",
  "entry": "index.js",
  "permissions": ["api:storage", "api:logging", "audio"],
  "hooks": ["onActivate", "onDeactivate", "onAudioProcess"]
}

index.js

class ReverbEffect {
  constructor(context) {
    this.api = context.api;
    this.enabled = false;
    this.intensity = 0.5;
  }

  async activate() {
    // Load saved settings
    const saved = await this.api.storage.get('reverbSettings');
    if (saved) {
      this.enabled = saved.enabled || false;
      this.intensity = saved.intensity || 0.5;
    }
    
    this.api.log(`Reverb effect ${this.enabled ? 'enabled' : 'disabled'}`);
    
    // Register UI controls (would be displayed in plugin settings)
    this.api.notify('Reverb effect ready! Use plugin settings to configure.', { duration: 3000 });
  }

  async onAudioProcess(audioData) {
    if (!this.enabled) return audioData;
    
    // Apply simple reverb simulation (delay + feedback)
    const processed = new Float32Array(audioData.length);
    const delaySamples = Math.floor(44100 * 0.3); // 300ms delay at 44.1kHz
    
    for (let i = 0; i < audioData.length; i++) {
      let sample = audioData[i];
      
      // Add delayed signal
      if (i >= delaySamples) {
        sample += audioData[i - delaySamples] * this.intensity * 0.5;
      }
      if (i >= delaySamples * 2) {
        sample += audioData[i - delaySamples * 2] * this.intensity * 0.25;
      }
      
      // Avoid clipping
      processed[i] = Math.max(-1, Math.min(1, sample));
    }
    
    return processed;
  }

  async setEnabled(enabled) {
    this.enabled = enabled;
    await this.api.storage.set('reverbSettings', {
      enabled: this.enabled,
      intensity: this.intensity
    });
    this.api.log(`Reverb ${enabled ? 'enabled' : 'disabled'}`);
  }

  async setIntensity(value) {
    this.intensity = Math.max(0, Math.min(1, value));
    await this.api.storage.set('reverbSettings', {
      enabled: this.enabled,
      intensity: this.intensity
    });
  }

  async deactivate() {
    this.enabled = false;
    this.api.log('Reverb effect deactivated');
  }
}

module.exports = ReverbEffect;

Example 4: Custom UI Plugin with CSS

Creates a floating panel with custom styles.

manifest.json

{
  "id": "com.example.stats-panel",
  "name": "Stats Panel",
  "version": "1.0.0",
  "entry": "index.js",
  "permissions": ["api:logging", "api:storage"],
  "hooks": ["onTrackPlay", "onTrackPause"]
}

ui.css

.stats-floating-panel {
  position: fixed;
  bottom: 100px;
  right: 20px;
  width: 280px;
  background: linear-gradient(135deg, rgba(26,26,36,0.95), rgba(18,18,26,0.95));
  backdrop-filter: blur(12px);
  border: 1px solid rgba(0,255,213,0.2);
  border-radius: 16px;
  padding: 16px;
  z-index: 1000;
  font-family: monospace;
  transition: transform 0.3s ease;
}

.stats-floating-panel:hover {
  transform: translateY(-4px);
  border-color: rgba(0,255,213,0.4);
}

.stats-title {
  font-size: 12px;
  font-weight: bold;
  color: #00ffd5;
  margin-bottom: 12px;
  display: flex;
  align-items: center;
  gap: 8px;
}

.stats-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 8px;
  font-size: 11px;
  color: #c7ced3;
}

.stats-value {
  color: #00ffd5;
  font-weight: bold;
}

.stats-progress-bar {
  height: 3px;
  background: rgba(255,255,255,0.1);
  border-radius: 3px;
  margin-top: 8px;
  overflow: hidden;
}

.stats-progress-fill {
  height: 100%;
  background: linear-gradient(90deg, #00ffd5, #00a884);
  width: 0%;
  transition: width 0.3s ease;
}

ui.js

// This script runs in the main renderer context
// and can interact with the plugin via postMessage

(function() {
  // Listen for messages from the plugin
  window.addEventListener('korai-plugin-message', (event) => {
    const { pluginId, message } = event.detail;
    if (pluginId === 'com.example.stats-panel') {
      updateStats(message);
    }
  });
  
  function updateStats(data) {
    const titleEl = document.querySelector('.stats-title span');
    if (titleEl && data.trackTitle) {
      titleEl.textContent = data.trackTitle;
    }
    
    const rows = document.querySelectorAll('.stats-row');
    if (rows.length >= 4 && data) {
      rows[0].querySelector('.stats-value').textContent = data.bpm || '--';
      rows[1].querySelector('.stats-value').textContent = data.energy ? `${Math.round(data.energy * 100)}%` : '--';
      rows[2].querySelector('.stats-value').textContent = data.plays || '0';
      rows[3].querySelector('.stats-value').textContent = data.likes || '0';
      
      const fill = document.querySelector('.stats-progress-fill');
      if (fill && data.energy) {
        fill.style.width = `${data.energy * 100}%`;
      }
    }
  }
})();

index.js

class StatsPanel {
  constructor(context) {
    this.api = context.api;
    this.statsInterval = null;
  }

  async activate() {
    this.api.log('Stats Panel activated');
    this.createPanel();
    this.startStatsUpdate();
  }

  createPanel() {
    // Check if panel already exists
    if (document.getElementById('stats-floating-panel')) return;
    
    const panel = document.createElement('div');
    panel.id = 'stats-floating-panel';
    panel.className = 'stats-floating-panel';
    panel.innerHTML = `
      <div class="stats-title">
        <i class="fa-solid fa-chart-line"></i>
        <span>Waiting for playback...</span>
      </div>
      <div class="stats-row">
        <span>🎵 BPM</span>
        <span class="stats-value">--</span>
      </div>
      <div class="stats-row">
        <span>⚡ Energy</span>
        <span class="stats-value">--</span>
      </div>
      <div class="stats-row">
        <span>▶️ Plays</span>
        <span class="stats-value">0</span>
      </div>
      <div class="stats-row">
        <span>❤️ Likes</span>
        <span class="stats-value">0</span>
      </div>
      <div class="stats-progress-bar">
        <div class="stats-progress-fill"></div>
      </div>
    `;
    
    document.body.appendChild(panel);
    this.panel = panel;
  }

  startStatsUpdate() {
    this.statsInterval = setInterval(async () => {
      if (!this.currentTrack) return;
      
      // Get current stats (you'd normally fetch these from API)
      const message = {
        trackTitle: this.currentTrack.title,
        bpm: this.currentTrack.bpm,
        energy: this.currentTrack.energy,
        plays: this.currentTrack.playCount,
        likes: this.currentTrack.likeCount
      };
      
      // Send to UI via postMessage
      const event = new CustomEvent('korai-plugin-message', {
        detail: {
          pluginId: 'com.example.stats-panel',
          message: message
        }
      });
      window.dispatchEvent(event);
      
    }, 1000);
  }

  async onTrackPlay(track) {
    this.currentTrack = track;
    this.api.log(`Stats panel tracking: ${track.title}`);
  }

  async onTrackPause() {
    // Optionally handle pause
  }

  async deactivate() {
    if (this.statsInterval) {
      clearInterval(this.statsInterval);
    }
    if (this.panel) {
      this.panel.remove();
    }
    this.api.log('Stats Panel deactivated');
  }
}

module.exports = StatsPanel;

Best Practices

1. Storage Usage

  • Use api.storage for persistent data, not global variables
  • Keep stored data under 10MB per plugin
  • Clean up old data in deactivate()

2. Performance

  • Avoid heavy synchronous operations in hooks
  • Use async/await for all API calls
  • Throttle high-frequency hooks like onAudioProcess
  • Release resources in deactivate()
let lastProcessTime = 0;
const THROTTLE_MS = 50;

async onAudioProcess(data) {
  const now = Date.now();
  if (now - lastProcessTime < THROTTLE_MS) return;
  lastProcessTime = now;
  
  // Process audio...
}

3. Error Handling

  • Always wrap async operations in try/catch
  • Use api.log() for debugging
  • Gracefully handle missing permissions
async onTrackPlay(track) {
  try {
    await this.processTrack(track);
  } catch (error) {
    this.api.log(`Error processing track: ${error.message}`);
    // Don't crash the plugin
  }
}

4. UI Integration

  • Use ui.css and ui.js for custom UI
  • Don't modify core DOM outside your containers
  • Clean up DOM elements in deactivate()

5. Versioning

  • Follow semantic versioning (MAJOR.MINOR.PATCH)
  • Increment version for any manifest changes
  • Test upgrades from previous versions

6. Security

  • Request minimal permissions
  • Don't store sensitive data in storage
  • Validate external input
  • Use api.requestPermission() for privileged operations

Troubleshooting

Common Issues

Plugin not appearing in Plugin Manager

  • Verify manifest.json is valid JSON
  • Check plugin directory is in correct location (userData/plugins/)
  • Ensure id field is unique

Plugin fails to activate

  • Check console for errors (View → Toggle Developer Tools)
  • Verify all permissions are granted
  • Ensure index.js exports a class or object with activate()

Storage not persisting

  • Use await api.storage.set(key, value) with await
  • Check if api:storage permission is granted

Hooks not firing

  • Declare hooks in manifest.json under "hooks" array
  • Ensure method names match exactly (case-sensitive)

Debugging

Enable verbose logging:

class MyPlugin {
  async activate() {
    this.api.log('=== PLUGIN DEBUG START ===');
    this.api.log(`Context: ${JSON.stringify(this.context)}`);
    this.api.log('=== PLUGIN DEBUG END ===');
  }
}

Getting Help

  • Check console output in Developer Tools (F12)
  • Review KORAI logs at userData/logs/korai.log
  • Visit GitHub Issues

Distributing Plugins

  1. Pack your plugin:

    korai-plugin pack ./my-plugin
  2. Test the ZIP:

    • Install in KORAI via Plugin Manager → Install Plugin
    • Verify functionality
  3. Share:

    • Upload to your website or GitHub
    • Share the .zip file
  4. Marketplace (coming soon):

    • Submit to official KORAI Plugin Store

Version History

Version Changes
1.0.0 Initial plugin system release
1.1.0 Added UI CSS/JS support
1.2.0 Added runtime permission requests
1.3.0 Added file system access APIs
1.4.0 Added custom hook registration

License

KORAI Plugin API is licensed under Apache 2.0 with Commons Clause. Plugins can use any license, but must comply with KORAI's terms of service.

Clone this wiki locally