spotube/LYRICS_PLUGIN_GUIDE.md
2025-12-07 23:36:39 +08:00

6.7 KiB

Lyrics Plugin Development Guide

This guide explains how to create a lyrics plugin for Spotube, similar to the existing audio source plugins.

Overview

Spotube uses Hetu Script for its plugin system. Lyrics plugins are packaged as .smplug files (ZIP archives) containing:

  • plugin.json - Plugin metadata and configuration
  • plugin.out - Compiled Hetu bytecode
  • logo.png - Optional plugin logo

Plugin Structure

your-lyrics-plugin/
├── plugin.json          # Plugin configuration
├── src/
│   └── main.ht         # Hetu script source
├── logo.png            # Optional logo
└── README.md

1. Create plugin.json

{
  "name": "LrcLib Lyrics",
  "author": "YourName",
  "description": "Lyrics provider using LrcLib API",
  "version": "1.0.0",
  "pluginApiVersion": "2.0.0",
  "entryPoint": "LrcLibLyricsPlugin",
  "repository": "https://github.com/yourusername/spotube-plugin-lrclib-lyrics",
  "apis": [],
  "abilities": ["lyrics"]
}

Key Fields:

  • name: Display name of your plugin
  • author: Your name or organization
  • version: Plugin version (semver)
  • pluginApiVersion: Must be "2.0.0" for current Spotube
  • entryPoint: Main class name in your Hetu script
  • abilities: Must include "lyrics" for lyrics plugins
  • apis: Empty array for lyrics plugins (used for metadata/audio plugins)

2. Write Hetu Script (src/main.ht)

import 'package:spotube_plugin/spotube_plugin.dart'

class LrcLibLyricsPlugin {
  
  // Called when plugin is loaded
  LrcLibLyricsPlugin() {
    print('LrcLib Lyrics Plugin initialized')
  }

  // Lyrics API endpoint
  var lyrics = LyricsEndpoint()
}

class LyricsEndpoint {
  
  // Search for lyrics
  // Returns: List<Map<String, dynamic>>
  external fun search(Map params) async {
    final trackName = params['trackName']
    final artistName = params['artistName']
    final albumName = params['albumName']
    final duration = params['duration']  // in seconds
    
    // Make HTTP request to LrcLib API
    final url = 'https://lrclib.net/api/search?track_name=$trackName&artist_name=$artistName'
    
    final response = await http.get(url)
    final data = json.decode(response.body)
    
    // Transform to Spotube format
    final results = []
    for (var item in data) {
      results.add({
        'id': item['id'].toString(),
        'name': item['name'],
        'uri': 'lrclib:${item['id']}',
        'rating': item['rating'] ?? 0,
        'provider': 'lrclib',
        'lyrics': parseLrc(item['syncedLyrics'] ?? item['plainLyrics'])
      })
    }
    
    return results
  }
  
  // Get lyrics by ID
  // Returns: Map<String, dynamic> or null
  external fun getById(String id) async {
    final url = 'https://lrclib.net/api/get/$id'
    
    final response = await http.get(url)
    if (response.statusCode != 200) {
      return null
    }
    
    final data = json.decode(response.body)
    
    return {
      'id': data['id'].toString(),
      'name': data['name'],
      'uri': 'lrclib:${data['id']}',
      'rating': data['rating'] ?? 0,
      'provider': 'lrclib',
      'lyrics': parseLrc(data['syncedLyrics'] ?? data['plainLyrics'])
    }
  }
  
  // Check if service is available
  external fun isAvailable() async {
    try {
      final response = await http.get('https://lrclib.net/api/health')
      return response.statusCode == 200
    } catch (e) {
      return false
    }
  }
  
  // Parse LRC format to lyrics array
  fun parseLrc(String lrcContent) {
    if (lrcContent == null || lrcContent.isEmpty) {
      return []
    }
    
    final lines = lrcContent.split('\n')
    final lyrics = []
    
    for (var line in lines) {
      // Parse [mm:ss.xx] format
      final match = RegExp(r'\[(\d+):(\d+)\.(\d+)\](.*)').firstMatch(line)
      if (match != null) {
        final minutes = int.parse(match.group(1))
        final seconds = int.parse(match.group(2))
        final centiseconds = int.parse(match.group(3))
        final text = match.group(4).trim()
        
        final timeMs = (minutes * 60 + seconds) * 1000 + centiseconds * 10
        
        lyrics.add({
          'time': timeMs,
          'text': text
        })
      }
    }
    
    return lyrics
  }
}

3. Expected Data Format

Search Parameters (input)

{
  'trackName': String,
  'artistName': String,
  'albumName': String?,  // optional
  'duration': int?       // optional, in seconds
}

Search Results (output)

[
  {
    'id': String,
    'name': String,
    'uri': String,
    'rating': int,
    'provider': String,
    'lyrics': [
      {
        'time': int,  // milliseconds
        'text': String
      }
    ]
  }
]

4. Compile Plugin

Install Hetu Compiler

dart pub global activate hetu_script_dev_tools

Compile Script

hetu compile src/main.ht -o plugin.out

5. Package Plugin

Create a ZIP file with .smplug extension:

zip your-plugin.smplug plugin.json plugin.out logo.png

Or using PowerShell:

Compress-Archive -Path plugin.json,plugin.out,logo.png -DestinationPath your-plugin.smplug

6. Test Plugin

  1. Open Spotube
  2. Go to Settings > Plugins
  3. Click "Add Plugin"
  4. Paste your plugin URL or select the .smplug file
  5. Set as default lyrics provider

7. Distribute Plugin

GitHub Release

  1. Create a GitHub repository
  2. Create a release with your .smplug file
  3. Users can install by pasting the repo URL

Direct Download

Host the .smplug file and share the direct download link

Example Plugins to Reference

  • spotube-plugin-youtube-audio: Audio source plugin
  • spotube-plugin-musicbrainz-listenbrainz: Metadata plugin

API Reference

Available in Hetu Scripts

  • http.get(url) - HTTP GET request
  • http.post(url, body) - HTTP POST request
  • json.encode(obj) - JSON encoding
  • json.decode(str) - JSON decoding
  • localStorage.get(key) - Get stored value
  • localStorage.set(key, value) - Store value
  • print(message) - Debug logging

Common Issues

  1. Plugin API Version Mismatch: Ensure pluginApiVersion is "2.0.0"
  2. Entry Point Not Found: Class name must match entryPoint in plugin.json
  3. Compilation Errors: Check Hetu syntax, use external fun for async methods
  4. Missing Abilities: Must include "lyrics" in abilities array

Testing Checklist

  • Plugin loads without errors
  • Search returns results for known songs
  • Lyrics are time-synced correctly
  • getById returns correct lyrics
  • isAvailable returns true when service is up
  • Handles network errors gracefully
  • Works on all platforms (Windows, macOS, Linux, Android, iOS)

Support

For questions and issues: