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

6.4 KiB

Lyrics Plugin Template Repository

This document provides a complete template for creating a lyrics plugin repository.

Repository Structure

spotube-plugin-lrclib-lyrics/
├── .github/
│   └── workflows/
│       └── release.yml
├── src/
│   └── main.ht
├── plugin.json
├── logo.png
├── README.md
├── LICENSE
└── .gitignore

File Contents

plugin.json

{
  "name": "LrcLib Lyrics",
  "author": "YourName",
  "description": "Free and open-source 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"]
}

src/main.ht

import 'package:spotube_plugin/spotube_plugin.dart'

class LrcLibLyricsPlugin {
  
  LrcLibLyricsPlugin() {
    print('LrcLib Lyrics Plugin v1.0.0 initialized')
  }

  var lyrics = LyricsEndpoint()
}

class LyricsEndpoint {
  
  external fun search(Map params) async {
    final trackName = params['trackName']
    final artistName = params['artistName']
    final albumName = params['albumName']
    final duration = params['duration']
    
    var url = 'https://lrclib.net/api/search?track_name=${Uri.encodeComponent(trackName)}&artist_name=${Uri.encodeComponent(artistName)}'
    
    if (albumName != null && albumName.isNotEmpty) {
      url += '&album_name=${Uri.encodeComponent(albumName)}'
    }
    
    try {
      final response = await http.get(url)
      
      if (response.statusCode != 200) {
        print('LrcLib API error: ${response.statusCode}')
        return []
      }
      
      final data = json.decode(response.body)
      
      if (data is! List) {
        return []
      }
      
      final results = []
      for (var item in data) {
        final syncedLyrics = item['syncedLyrics']
        final plainLyrics = item['plainLyrics']
        
        if (syncedLyrics == null && plainLyrics == null) {
          continue
        }
        
        results.add({
          'uri': 'lrclib:${item['id']}',
          'name': '${item['trackName']} - ${item['artistName']}',
          'lyrics': parseLrc(syncedLyrics ?? plainLyrics ?? ''),
          'rating': 5,
          'provider': 'lrclib'
        })
      }
      
      return results
    } catch (e) {
      print('LrcLib search error: $e')
      return []
    }
  }
  
  external fun getById(String id) async {
    final url = 'https://lrclib.net/api/get/$id'
    
    try {
      final response = await http.get(url)
      
      if (response.statusCode != 200) {
        return null
      }
      
      final data = json.decode(response.body)
      final syncedLyrics = data['syncedLyrics']
      final plainLyrics = data['plainLyrics']
      
      if (syncedLyrics == null && plainLyrics == null) {
        return null
      }
      
      return {
        'uri': 'lrclib:${data['id']}',
        'name': '${data['trackName']} - ${data['artistName']}',
        'lyrics': parseLrc(syncedLyrics ?? plainLyrics ?? ''),
        'rating': 5,
        'provider': 'lrclib'
      }
    } catch (e) {
      print('LrcLib getById error: $e')
      return null
    }
  }
  
  external fun isAvailable() async {
    try {
      final response = await http.get('https://lrclib.net/')
      return response.statusCode == 200
    } catch (e) {
      return false
    }
  }
  
  fun parseLrc(String lrcContent) {
    if (lrcContent.isEmpty) {
      return []
    }
    
    final lines = lrcContent.split('\n')
    final lyrics = []
    
    for (var line in lines) {
      final timeMatch = RegExp(r'\[(\d+):(\d+)\.(\d+)\]').firstMatch(line)
      
      if (timeMatch != null) {
        final minutes = int.parse(timeMatch.group(1))
        final seconds = int.parse(timeMatch.group(2))
        final centiseconds = int.parse(timeMatch.group(3))
        
        final text = line.substring(timeMatch.end).trim()
        
        if (text.isNotEmpty) {
          final timeMs = (minutes * 60 + seconds) * 1000 + centiseconds * 10
          
          lyrics.add({
            'time': timeMs,
            'text': text
          })
        }
      }
    }
    
    return lyrics
  }
}

README.md

# LrcLib Lyrics Plugin for Spotube

Free and open-source lyrics provider using the LrcLib API.

## Features

- Time-synced lyrics
- Free and open-source
- No API key required
- Large lyrics database

## Installation

1. Open Spotube
2. Go to Settings > Plugins
3. Click "Add Plugin"
4. Paste: `https://github.com/yourusername/spotube-plugin-lrclib-lyrics`
5. Click Install

## Building

### Prerequisites
- Dart SDK
- Hetu Script Dev Tools

### Compile
```bash
dart pub global activate hetu_script_dev_tools
hetu compile src/main.ht -o plugin.out

Package

zip spotube-plugin-lrclib-lyrics.smplug plugin.json plugin.out logo.png

API

This plugin uses the LrcLib API.

License

MIT License


### .github/workflows/release.yml

```yaml
name: Release Plugin

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - uses: dart-lang/setup-dart@v1
      
      - name: Install Hetu
        run: dart pub global activate hetu_script_dev_tools
      
      - name: Compile Plugin
        run: hetu compile src/main.ht -o plugin.out
      
      - name: Package Plugin
        run: zip spotube-plugin-lrclib-lyrics.smplug plugin.json plugin.out logo.png
      
      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          files: spotube-plugin-lrclib-lyrics.smplug
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

plugin.out
*.smplug
.DS_Store

Publishing

  1. Create repository on GitHub
  2. Push code
  3. Create a tag: git tag v1.0.0
  4. Push tag: git push origin v1.0.0
  5. GitHub Actions will automatically build and release

Testing Locally

  1. Compile: hetu compile src/main.ht -o plugin.out
  2. Package: zip plugin.smplug plugin.json plugin.out logo.png
  3. In Spotube, add plugin via file path

Example Plugins