From d13e9685f3a90399e8873827148041ed07321af8 Mon Sep 17 00:00:00 2001 From: thumb2086 Date: Sun, 7 Dec 2025 23:36:39 +0800 Subject: [PATCH] feat: Add lyrics plugin system support --- LYRICS_PLUGIN_GUIDE.md | 271 ++++++++++++++++++ LYRICS_PLUGIN_MIGRATION.md | 207 ++++++++++++++ LYRICS_PLUGIN_TEMPLATE.md | 289 ++++++++++++++++++++ lib/models/metadata/plugin.dart | 1 + lib/provider/lyrics_plugin_provider.dart | 55 ++++ lib/services/lyrics_provider/README.md | 79 ++++++ lib/services/metadata/endpoints/lyrics.dart | 51 ++++ lib/services/metadata/metadata.dart | 4 + 8 files changed, 957 insertions(+) create mode 100644 LYRICS_PLUGIN_GUIDE.md create mode 100644 LYRICS_PLUGIN_MIGRATION.md create mode 100644 LYRICS_PLUGIN_TEMPLATE.md create mode 100644 lib/provider/lyrics_plugin_provider.dart create mode 100644 lib/services/lyrics_provider/README.md create mode 100644 lib/services/metadata/endpoints/lyrics.dart diff --git a/LYRICS_PLUGIN_GUIDE.md b/LYRICS_PLUGIN_GUIDE.md new file mode 100644 index 00000000..8aba600d --- /dev/null +++ b/LYRICS_PLUGIN_GUIDE.md @@ -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> + 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 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 diff --git a/LYRICS_PLUGIN_MIGRATION.md b/LYRICS_PLUGIN_MIGRATION.md new file mode 100644 index 00000000..dc51c6c9 --- /dev/null +++ b/LYRICS_PLUGIN_MIGRATION.md @@ -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 │ +│ - 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 diff --git a/LYRICS_PLUGIN_TEMPLATE.md b/LYRICS_PLUGIN_TEMPLATE.md new file mode 100644 index 00000000..dba6e3e4 --- /dev/null +++ b/LYRICS_PLUGIN_TEMPLATE.md @@ -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) diff --git a/lib/models/metadata/plugin.dart b/lib/models/metadata/plugin.dart index 6bc84160..f05ba7d9 100644 --- a/lib/models/metadata/plugin.dart +++ b/lib/models/metadata/plugin.dart @@ -8,6 +8,7 @@ enum PluginAbilities { metadata, @JsonValue('audio-source') audioSource, + lyrics, } @freezed diff --git a/lib/provider/lyrics_plugin_provider.dart b/lib/provider/lyrics_plugin_provider.dart new file mode 100644 index 00000000..93c96de2 --- /dev/null +++ b/lib/provider/lyrics_plugin_provider.dart @@ -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( + (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, Map>( + (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( + (ref, id) async { + final plugin = await ref.watch(lyricsPluginProvider.future); + + if (plugin == null) { + return null; + } + + return await plugin.lyrics.getById(id); + }, +); diff --git a/lib/services/lyrics_provider/README.md b/lib/services/lyrics_provider/README.md new file mode 100644 index 00000000..aa1c00e9 --- /dev/null +++ b/lib/services/lyrics_provider/README.md @@ -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>> search(Map params); + Future?> getById(String id); + Future 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 diff --git a/lib/services/metadata/endpoints/lyrics.dart b/lib/services/metadata/endpoints/lyrics.dart new file mode 100644 index 00000000..e2717a05 --- /dev/null +++ b/lib/services/metadata/endpoints/lyrics.dart @@ -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> 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 getById(String id) async { + final result = await hetuMetadataLyrics.invoke( + "getById", + positionalArgs: [id], + ); + + if (result == null) return null; + + return SubtitleSimple.fromJson(result); + } + + Future isAvailable() async { + final result = await hetuMetadataLyrics.invoke("isAvailable"); + return result as bool? ?? false; + } +} diff --git a/lib/services/metadata/metadata.dart b/lib/services/metadata/metadata.dart index 5860e0d6..d5385839 100644 --- a/lib/services/metadata/metadata.dart +++ b/lib/services/metadata/metadata.dart @@ -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); } }