feat: Add lyrics plugin system support

This commit is contained in:
thumb2086 2025-12-07 23:36:39 +08:00
parent b254ab6fe2
commit d13e9685f3
8 changed files with 957 additions and 0 deletions

271
LYRICS_PLUGIN_GUIDE.md Normal file
View 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
View 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
View 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)

View File

@ -8,6 +8,7 @@ enum PluginAbilities {
metadata,
@JsonValue('audio-source')
audioSource,
lyrics,
}
@freezed

View 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);
},
);

View 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

View 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;
}
}

View File

@ -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);
}
}