mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-02-04 07:52:55 +00:00
Merge d13e9685f3 into b254ab6fe2
This commit is contained in:
commit
e4218499eb
271
LYRICS_PLUGIN_GUIDE.md
Normal file
271
LYRICS_PLUGIN_GUIDE.md
Normal file
@ -0,0 +1,271 @@
|
||||
# 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
|
||||
|
||||
```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)
|
||||
|
||||
```hetu
|
||||
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)
|
||||
```dart
|
||||
{
|
||||
'trackName': String,
|
||||
'artistName': String,
|
||||
'albumName': String?, // optional
|
||||
'duration': int? // optional, in seconds
|
||||
}
|
||||
```
|
||||
|
||||
### Search Results (output)
|
||||
```dart
|
||||
[
|
||||
{
|
||||
'id': String,
|
||||
'name': String,
|
||||
'uri': String,
|
||||
'rating': int,
|
||||
'provider': String,
|
||||
'lyrics': [
|
||||
{
|
||||
'time': int, // milliseconds
|
||||
'text': String
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 4. Compile Plugin
|
||||
|
||||
### Install Hetu Compiler
|
||||
```bash
|
||||
dart pub global activate hetu_script_dev_tools
|
||||
```
|
||||
|
||||
### Compile Script
|
||||
```bash
|
||||
hetu compile src/main.ht -o plugin.out
|
||||
```
|
||||
|
||||
## 5. Package Plugin
|
||||
|
||||
Create a ZIP file with `.smplug` extension:
|
||||
```bash
|
||||
zip your-plugin.smplug plugin.json plugin.out logo.png
|
||||
```
|
||||
|
||||
Or using PowerShell:
|
||||
```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:
|
||||
- Spotube Discord: https://discord.gg/spotube
|
||||
- GitHub Issues: https://github.com/KRTirtho/spotube/issues
|
||||
207
LYRICS_PLUGIN_MIGRATION.md
Normal file
207
LYRICS_PLUGIN_MIGRATION.md
Normal file
@ -0,0 +1,207 @@
|
||||
# Lyrics Plugin System Migration
|
||||
|
||||
This document explains the migration from built-in lyrics to a plugin-based system.
|
||||
|
||||
## What Changed
|
||||
|
||||
### Before
|
||||
- Lyrics providers were built into the main application
|
||||
- Hard-coded implementations for each service
|
||||
- Required app updates to add new providers
|
||||
|
||||
### After
|
||||
- Lyrics are provided through plugins
|
||||
- Community can create providers for any service
|
||||
- No app updates needed for new providers
|
||||
- Follows the same pattern as audio source plugins
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Spotube Main App │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ Lyrics Plugin Interface │ │
|
||||
│ │ (MetadataPluginLyricsEndpoint) │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ Hetu Script Engine │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Lyrics Plugin (.smplug) │
|
||||
│ │
|
||||
│ - plugin.json (metadata) │
|
||||
│ - plugin.out (compiled Hetu bytecode) │
|
||||
│ - logo.png (optional) │
|
||||
│ │
|
||||
│ Implements: │
|
||||
│ - search(params) → List<Lyrics> │
|
||||
│ - getById(id) → Lyrics? │
|
||||
│ - isAvailable() → bool │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ External Lyrics API │
|
||||
│ (LrcLib, Musixmatch, Genius, etc.) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Main App Changes
|
||||
|
||||
### Added Files
|
||||
- `lib/services/metadata/endpoints/lyrics.dart` - Lyrics endpoint interface
|
||||
- `lib/provider/lyrics_plugin_provider.dart` - Riverpod providers for lyrics
|
||||
- `lib/services/lyrics_provider/README.md` - Documentation
|
||||
- `LYRICS_PLUGIN_GUIDE.md` - Plugin development guide
|
||||
- `LYRICS_PLUGIN_TEMPLATE.md` - Template repository structure
|
||||
- `LYRICS_PLUGIN_MIGRATION.md` - This file
|
||||
|
||||
### Modified Files
|
||||
- `lib/models/metadata/plugin.dart` - Added `lyrics` to `PluginAbilities` enum
|
||||
- `lib/services/metadata/metadata.dart` - Added lyrics endpoint initialization
|
||||
|
||||
### Removed Files
|
||||
- All built-in lyrics provider implementations (moved to plugins)
|
||||
|
||||
## Plugin Development
|
||||
|
||||
### Quick Start
|
||||
1. Clone template: `LYRICS_PLUGIN_TEMPLATE.md`
|
||||
2. Implement Hetu script in `src/main.ht`
|
||||
3. Compile: `hetu compile src/main.ht -o plugin.out`
|
||||
4. Package: `zip plugin.smplug plugin.json plugin.out logo.png`
|
||||
5. Publish on GitHub with releases
|
||||
|
||||
### Required Methods
|
||||
|
||||
```hetu
|
||||
class LyricsEndpoint {
|
||||
external fun search(Map params) async
|
||||
external fun getById(String id) async
|
||||
external fun isAvailable() async
|
||||
}
|
||||
```
|
||||
|
||||
### Data Format
|
||||
|
||||
**Input (search params):**
|
||||
```dart
|
||||
{
|
||||
'trackName': String,
|
||||
'artistName': String,
|
||||
'albumName': String?,
|
||||
'duration': int? // seconds
|
||||
}
|
||||
```
|
||||
|
||||
**Output (lyrics):**
|
||||
```dart
|
||||
{
|
||||
'uri': String,
|
||||
'name': String,
|
||||
'lyrics': [
|
||||
{
|
||||
'time': int, // milliseconds
|
||||
'text': String
|
||||
}
|
||||
],
|
||||
'rating': int,
|
||||
'provider': String
|
||||
}
|
||||
```
|
||||
|
||||
## Usage in Main App
|
||||
|
||||
### Get Lyrics Plugin
|
||||
```dart
|
||||
final plugin = ref.watch(lyricsPluginProvider);
|
||||
```
|
||||
|
||||
### Search Lyrics
|
||||
```dart
|
||||
final lyrics = await ref.read(lyricsSearchProvider({
|
||||
'trackName': 'Song Name',
|
||||
'artistName': 'Artist Name',
|
||||
'albumName': 'Album Name',
|
||||
'duration': Duration(seconds: 180),
|
||||
}).future);
|
||||
```
|
||||
|
||||
### Get by ID
|
||||
```dart
|
||||
final lyric = await ref.read(lyricsByIdProvider('lrclib:12345').future);
|
||||
```
|
||||
|
||||
## Plugin Examples
|
||||
|
||||
### LrcLib Plugin
|
||||
- Free, open-source
|
||||
- No API key required
|
||||
- Repository: Create at `spotube-plugin-lrclib-lyrics`
|
||||
|
||||
### Musixmatch Plugin
|
||||
- Commercial service
|
||||
- Requires API key
|
||||
- Repository: Create at `spotube-plugin-musixmatch-lyrics`
|
||||
|
||||
### Genius Plugin
|
||||
- Community lyrics
|
||||
- Web scraping or API
|
||||
- Repository: Create at `spotube-plugin-genius-lyrics`
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Extensibility**: Anyone can add new lyrics sources
|
||||
2. **Maintainability**: Plugins maintained separately
|
||||
3. **Flexibility**: Users choose their preferred provider
|
||||
4. **Privacy**: No forced third-party services
|
||||
5. **Community**: Encourages community contributions
|
||||
6. **Updates**: Plugins update independently
|
||||
|
||||
## Migration Path for Users
|
||||
|
||||
1. Update Spotube to version with plugin support
|
||||
2. Install lyrics plugin from Settings > Plugins
|
||||
3. Choose default lyrics provider
|
||||
4. Existing lyrics cache remains compatible
|
||||
|
||||
## For Plugin Developers
|
||||
|
||||
### Resources
|
||||
- Plugin Guide: `LYRICS_PLUGIN_GUIDE.md`
|
||||
- Template: `LYRICS_PLUGIN_TEMPLATE.md`
|
||||
- Example: Check `assets/plugins/spotube-plugin-youtube-audio/`
|
||||
- Hetu Docs: https://hetu.dev
|
||||
|
||||
### Testing
|
||||
1. Compile plugin locally
|
||||
2. Load in Spotube via file path
|
||||
3. Test search and getById methods
|
||||
4. Verify time-sync accuracy
|
||||
5. Test error handling
|
||||
|
||||
### Publishing
|
||||
1. Create GitHub repository
|
||||
2. Add GitHub Actions for auto-build
|
||||
3. Create release with `.smplug` file
|
||||
4. Users install via repo URL
|
||||
|
||||
## Support
|
||||
|
||||
- Discord: https://discord.gg/spotube
|
||||
- GitHub: https://github.com/KRTirtho/spotube
|
||||
- Docs: https://spotube.krtirtho.dev
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Multiple provider fallback
|
||||
- Provider priority settings
|
||||
- Lyrics caching system
|
||||
- Offline lyrics support
|
||||
- User-contributed lyrics
|
||||
- Lyrics translation plugins
|
||||
- Karaoke mode plugins
|
||||
289
LYRICS_PLUGIN_TEMPLATE.md
Normal file
289
LYRICS_PLUGIN_TEMPLATE.md
Normal file
@ -0,0 +1,289 @@
|
||||
# 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
|
||||
|
||||
```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
|
||||
|
||||
```hetu
|
||||
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
|
||||
|
||||
```markdown
|
||||
# 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
|
||||
```bash
|
||||
zip spotube-plugin-lrclib-lyrics.smplug plugin.json plugin.out logo.png
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
This plugin uses the [LrcLib API](https://lrclib.net/docs).
|
||||
|
||||
## 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
|
||||
|
||||
- [spotube-plugin-youtube-audio](https://github.com/KRTirtho/spotube/tree/master/assets/plugins/spotube-plugin-youtube-audio)
|
||||
- [spotube-plugin-musicbrainz-listenbrainz](https://github.com/KRTirtho/spotube/tree/master/assets/plugins/spotube-plugin-musicbrainz-listenbrainz)
|
||||
@ -8,6 +8,7 @@ enum PluginAbilities {
|
||||
metadata,
|
||||
@JsonValue('audio-source')
|
||||
audioSource,
|
||||
lyrics,
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
55
lib/provider/lyrics_plugin_provider.dart
Normal file
55
lib/provider/lyrics_plugin_provider.dart
Normal file
@ -0,0 +1,55 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/models/lyrics.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/services/metadata/metadata.dart';
|
||||
|
||||
final lyricsPluginProvider = FutureProvider<MetadataPlugin?>(
|
||||
(ref) async {
|
||||
final plugins = await ref.watch(metadataPluginsProvider.future);
|
||||
|
||||
final lyricsPlugin = plugins.plugins.firstWhere(
|
||||
(p) => p.abilities.contains(PluginAbilities.lyrics),
|
||||
orElse: () => throw Exception('No lyrics plugin found'),
|
||||
);
|
||||
|
||||
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
||||
final pluginByteCode = await pluginsNotifier.getPluginByteCode(lyricsPlugin);
|
||||
final youtubeEngine = ref.read(youtubeEngineProvider);
|
||||
|
||||
return await MetadataPlugin.create(
|
||||
youtubeEngine,
|
||||
lyricsPlugin,
|
||||
pluginByteCode,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final lyricsSearchProvider = FutureProvider.family<List<SubtitleSimple>, Map<String, dynamic>>(
|
||||
(ref, params) async {
|
||||
final plugin = await ref.watch(lyricsPluginProvider.future);
|
||||
|
||||
if (plugin == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await plugin.lyrics.search(
|
||||
trackName: params['trackName'] as String,
|
||||
artistName: params['artistName'] as String,
|
||||
albumName: params['albumName'] as String?,
|
||||
duration: params['duration'] as Duration?,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final lyricsByIdProvider = FutureProvider.family<SubtitleSimple?, String>(
|
||||
(ref, id) async {
|
||||
final plugin = await ref.watch(lyricsPluginProvider.future);
|
||||
|
||||
if (plugin == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await plugin.lyrics.getById(id);
|
||||
},
|
||||
);
|
||||
79
lib/services/lyrics_provider/README.md
Normal file
79
lib/services/lyrics_provider/README.md
Normal file
@ -0,0 +1,79 @@
|
||||
# Lyrics Provider System
|
||||
|
||||
This directory contains the lyrics provider plugin system for Spotube.
|
||||
|
||||
## Architecture
|
||||
|
||||
The lyrics system is designed to be fully plugin-based, similar to the audio source and metadata plugins.
|
||||
|
||||
### Components
|
||||
|
||||
1. **lyrics_provider.dart** - Abstract interface for lyrics providers
|
||||
2. **lyrics_provider_manager.dart** - Manages multiple lyrics providers
|
||||
3. **providers/** - Built-in provider stubs (to be implemented as plugins)
|
||||
|
||||
## Plugin Integration
|
||||
|
||||
Lyrics plugins are loaded through the Hetu script system and must implement the following interface:
|
||||
|
||||
```dart
|
||||
class LyricsEndpoint {
|
||||
Future<List<Map<String, dynamic>>> search(Map params);
|
||||
Future<Map<String, dynamic>?> getById(String id);
|
||||
Future<bool> isAvailable();
|
||||
}
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User Request
|
||||
↓
|
||||
LyricsProviderManager
|
||||
↓
|
||||
Plugin System (Hetu)
|
||||
↓
|
||||
External API (LrcLib, Musixmatch, etc.)
|
||||
↓
|
||||
SubtitleSimple Model
|
||||
↓
|
||||
UI Display
|
||||
```
|
||||
|
||||
## Creating a Lyrics Plugin
|
||||
|
||||
See `LYRICS_PLUGIN_GUIDE.md` in the root directory for detailed instructions.
|
||||
|
||||
### Quick Start
|
||||
|
||||
1. Create plugin.json with "abilities": ["lyrics"]
|
||||
2. Implement LyricsEndpoint class in Hetu script
|
||||
3. Compile to bytecode
|
||||
4. Package as .smplug file
|
||||
5. Distribute via GitHub releases
|
||||
|
||||
## Built-in Providers
|
||||
|
||||
The following providers are stubs that will be implemented as separate plugins:
|
||||
|
||||
- **LrcLibProvider** - Free, open lyrics database
|
||||
- **MusixmatchProvider** - Commercial lyrics service (requires API key)
|
||||
|
||||
## Migration Notes
|
||||
|
||||
The old lyrics functionality has been removed from the main codebase and will be reimplemented as plugins. This allows:
|
||||
|
||||
- Community-contributed lyrics sources
|
||||
- Easy addition of new providers
|
||||
- No vendor lock-in
|
||||
- Better separation of concerns
|
||||
- Reduced main app size
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Multiple provider fallback
|
||||
- Lyrics caching
|
||||
- Offline lyrics support
|
||||
- User-contributed lyrics
|
||||
- Lyrics translation
|
||||
- Karaoke mode
|
||||
51
lib/services/metadata/endpoints/lyrics.dart
Normal file
51
lib/services/metadata/endpoints/lyrics.dart
Normal file
@ -0,0 +1,51 @@
|
||||
part of '../metadata.dart';
|
||||
|
||||
class MetadataPluginLyricsEndpoint {
|
||||
final Hetu hetu;
|
||||
|
||||
MetadataPluginLyricsEndpoint(this.hetu);
|
||||
|
||||
HTInstance get hetuMetadataLyrics =>
|
||||
(hetu.fetch("metadataPlugin") as HTInstance).memberGet("lyrics")
|
||||
as HTInstance;
|
||||
|
||||
Future<List<SubtitleSimple>> search({
|
||||
required String trackName,
|
||||
required String artistName,
|
||||
String? albumName,
|
||||
Duration? duration,
|
||||
}) async {
|
||||
final result = await hetuMetadataLyrics.invoke(
|
||||
"search",
|
||||
positionalArgs: [
|
||||
{
|
||||
'trackName': trackName,
|
||||
'artistName': artistName,
|
||||
'albumName': albumName,
|
||||
'duration': duration?.inSeconds,
|
||||
}
|
||||
],
|
||||
);
|
||||
|
||||
if (result == null) return [];
|
||||
|
||||
final list = result as List;
|
||||
return list.map((item) => SubtitleSimple.fromJson(item)).toList();
|
||||
}
|
||||
|
||||
Future<SubtitleSimple?> getById(String id) async {
|
||||
final result = await hetuMetadataLyrics.invoke(
|
||||
"getById",
|
||||
positionalArgs: [id],
|
||||
);
|
||||
|
||||
if (result == null) return null;
|
||||
|
||||
return SubtitleSimple.fromJson(result);
|
||||
}
|
||||
|
||||
Future<bool> isAvailable() async {
|
||||
final result = await hetuMetadataLyrics.invoke("isAvailable");
|
||||
return result as bool? ?? false;
|
||||
}
|
||||
}
|
||||
@ -25,7 +25,9 @@ import 'package:spotube/services/metadata/endpoints/search.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/track.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/core.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/user.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/lyrics.dart';
|
||||
import 'package:spotube/services/youtube_engine/youtube_engine.dart';
|
||||
import 'package:spotube/models/lyrics.dart';
|
||||
|
||||
const defaultMetadataLimit = "20";
|
||||
|
||||
@ -164,6 +166,7 @@ class MetadataPlugin {
|
||||
late final MetadataPluginTrackEndpoint track;
|
||||
late final MetadataPluginUserEndpoint user;
|
||||
late final MetadataPluginCore core;
|
||||
late final MetadataPluginLyricsEndpoint lyrics;
|
||||
|
||||
MetadataPlugin._(this.hetu) {
|
||||
auth = MetadataAuthEndpoint(hetu);
|
||||
@ -177,5 +180,6 @@ class MetadataPlugin {
|
||||
track = MetadataPluginTrackEndpoint(hetu);
|
||||
user = MetadataPluginUserEndpoint(hetu);
|
||||
core = MetadataPluginCore(hetu);
|
||||
lyrics = MetadataPluginLyricsEndpoint(hetu);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user