mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-19 06:38:08 +02:00
927 lines
33 KiB
Dart
927 lines
33 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:spotiflac_android/utils/logger.dart';
|
|
|
|
final _log = AppLogger('PlatformBridge');
|
|
|
|
/// Bridge to communicate with Go backend via platform channels
|
|
class PlatformBridge {
|
|
static const _channel = MethodChannel('com.zarz.spotiflac/backend');
|
|
|
|
/// Parse and validate Spotify URL
|
|
static Future<Map<String, dynamic>> parseSpotifyUrl(String url) async {
|
|
_log.d('parseSpotifyUrl: $url');
|
|
final result = await _channel.invokeMethod('parseSpotifyUrl', {'url': url});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Get Spotify metadata from URL
|
|
static Future<Map<String, dynamic>> getSpotifyMetadata(String url) async {
|
|
_log.d('getSpotifyMetadata: $url');
|
|
final result = await _channel.invokeMethod('getSpotifyMetadata', {'url': url});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Search Spotify
|
|
static Future<Map<String, dynamic>> searchSpotify(String query, {int limit = 10}) async {
|
|
_log.d('searchSpotify: "$query" (limit: $limit)');
|
|
final result = await _channel.invokeMethod('searchSpotify', {
|
|
'query': query,
|
|
'limit': limit,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Search Spotify for tracks and artists
|
|
static Future<Map<String, dynamic>> searchSpotifyAll(String query, {int trackLimit = 15, int artistLimit = 3}) async {
|
|
_log.d('searchSpotifyAll: "$query"');
|
|
final result = await _channel.invokeMethod('searchSpotifyAll', {
|
|
'query': query,
|
|
'track_limit': trackLimit,
|
|
'artist_limit': artistLimit,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Check track availability on streaming services
|
|
static Future<Map<String, dynamic>> checkAvailability(String spotifyId, String isrc) async {
|
|
_log.d('checkAvailability: $spotifyId (ISRC: $isrc)');
|
|
final result = await _channel.invokeMethod('checkAvailability', {
|
|
'spotify_id': spotifyId,
|
|
'isrc': isrc,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Download a track from specific service
|
|
static Future<Map<String, dynamic>> downloadTrack({
|
|
required String isrc,
|
|
required String service,
|
|
required String spotifyId,
|
|
required String trackName,
|
|
required String artistName,
|
|
required String albumName,
|
|
String? albumArtist,
|
|
String? coverUrl,
|
|
required String outputDir,
|
|
required String filenameFormat,
|
|
String quality = 'LOSSLESS',
|
|
bool embedLyrics = true,
|
|
bool embedMaxQualityCover = true,
|
|
int trackNumber = 1,
|
|
int discNumber = 1,
|
|
int totalTracks = 1,
|
|
String? releaseDate,
|
|
String? itemId,
|
|
int durationMs = 0,
|
|
}) async {
|
|
_log.i('downloadTrack: "$trackName" by $artistName via $service');
|
|
final request = jsonEncode({
|
|
'isrc': isrc,
|
|
'service': service,
|
|
'spotify_id': spotifyId,
|
|
'track_name': trackName,
|
|
'artist_name': artistName,
|
|
'album_name': albumName,
|
|
'album_artist': albumArtist ?? artistName,
|
|
'cover_url': coverUrl,
|
|
'output_dir': outputDir,
|
|
'filename_format': filenameFormat,
|
|
'quality': quality,
|
|
'embed_lyrics': embedLyrics,
|
|
'embed_max_quality_cover': embedMaxQualityCover,
|
|
'track_number': trackNumber,
|
|
'disc_number': discNumber,
|
|
'total_tracks': totalTracks,
|
|
'release_date': releaseDate ?? '',
|
|
'item_id': itemId ?? '',
|
|
'duration_ms': durationMs,
|
|
});
|
|
|
|
final result = await _channel.invokeMethod('downloadTrack', request);
|
|
final response = jsonDecode(result as String) as Map<String, dynamic>;
|
|
if (response['success'] == true) {
|
|
_log.i('Download success: ${response['file_path']}');
|
|
} else {
|
|
_log.w('Download failed: ${response['error']}');
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/// Download with automatic fallback to other services
|
|
static Future<Map<String, dynamic>> downloadWithFallback({
|
|
required String isrc,
|
|
required String spotifyId,
|
|
required String trackName,
|
|
required String artistName,
|
|
required String albumName,
|
|
String? albumArtist,
|
|
String? coverUrl,
|
|
required String outputDir,
|
|
required String filenameFormat,
|
|
String quality = 'LOSSLESS',
|
|
bool embedLyrics = true,
|
|
bool embedMaxQualityCover = true,
|
|
int trackNumber = 1,
|
|
int discNumber = 1,
|
|
int totalTracks = 1,
|
|
String? releaseDate,
|
|
String preferredService = 'tidal',
|
|
String? itemId,
|
|
int durationMs = 0,
|
|
}) async {
|
|
_log.i('downloadWithFallback: "$trackName" by $artistName (preferred: $preferredService)');
|
|
final request = jsonEncode({
|
|
'isrc': isrc,
|
|
'service': preferredService,
|
|
'spotify_id': spotifyId,
|
|
'track_name': trackName,
|
|
'artist_name': artistName,
|
|
'album_name': albumName,
|
|
'album_artist': albumArtist ?? artistName,
|
|
'cover_url': coverUrl,
|
|
'output_dir': outputDir,
|
|
'filename_format': filenameFormat,
|
|
'quality': quality,
|
|
'embed_lyrics': embedLyrics,
|
|
'embed_max_quality_cover': embedMaxQualityCover,
|
|
'track_number': trackNumber,
|
|
'disc_number': discNumber,
|
|
'total_tracks': totalTracks,
|
|
'release_date': releaseDate ?? '',
|
|
'item_id': itemId ?? '',
|
|
'duration_ms': durationMs,
|
|
});
|
|
|
|
final result = await _channel.invokeMethod('downloadWithFallback', request);
|
|
final response = jsonDecode(result as String) as Map<String, dynamic>;
|
|
if (response['success'] == true) {
|
|
final service = response['service'] ?? 'unknown';
|
|
final filePath = response['file_path'] ?? '';
|
|
final bitDepth = response['actual_bit_depth'];
|
|
final sampleRate = response['actual_sample_rate'];
|
|
final qualityStr = bitDepth != null && sampleRate != null
|
|
? ' ($bitDepth-bit/${(sampleRate / 1000).toStringAsFixed(1)}kHz)'
|
|
: '';
|
|
_log.i('Download success via $service$qualityStr: $filePath');
|
|
} else {
|
|
final error = response['error'] ?? 'Unknown error';
|
|
final errorType = response['error_type'] ?? '';
|
|
_log.e('Download failed: $error (type: $errorType)');
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/// Get download progress (legacy single download)
|
|
static Future<Map<String, dynamic>> getDownloadProgress() async {
|
|
final result = await _channel.invokeMethod('getDownloadProgress');
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Get progress for all active downloads (concurrent mode)
|
|
static Future<Map<String, dynamic>> getAllDownloadProgress() async {
|
|
final result = await _channel.invokeMethod('getAllDownloadProgress');
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Initialize progress tracking for a download item
|
|
static Future<void> initItemProgress(String itemId) async {
|
|
await _channel.invokeMethod('initItemProgress', {'item_id': itemId});
|
|
}
|
|
|
|
/// Finish progress tracking for a download item
|
|
static Future<void> finishItemProgress(String itemId) async {
|
|
await _channel.invokeMethod('finishItemProgress', {'item_id': itemId});
|
|
}
|
|
|
|
/// Clear progress tracking for a download item
|
|
static Future<void> clearItemProgress(String itemId) async {
|
|
await _channel.invokeMethod('clearItemProgress', {'item_id': itemId});
|
|
}
|
|
|
|
/// Cancel an in-progress download
|
|
static Future<void> cancelDownload(String itemId) async {
|
|
await _channel.invokeMethod('cancelDownload', {'item_id': itemId});
|
|
}
|
|
|
|
/// Set download directory
|
|
static Future<void> setDownloadDirectory(String path) async {
|
|
await _channel.invokeMethod('setDownloadDirectory', {'path': path});
|
|
}
|
|
|
|
/// Check if file with ISRC already exists
|
|
static Future<Map<String, dynamic>> checkDuplicate(String outputDir, String isrc) async {
|
|
final result = await _channel.invokeMethod('checkDuplicate', {
|
|
'output_dir': outputDir,
|
|
'isrc': isrc,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Build filename from template
|
|
static Future<String> buildFilename(String template, Map<String, dynamic> metadata) async {
|
|
final result = await _channel.invokeMethod('buildFilename', {
|
|
'template': template,
|
|
'metadata': jsonEncode(metadata),
|
|
});
|
|
return result as String;
|
|
}
|
|
|
|
/// Sanitize filename
|
|
static Future<String> sanitizeFilename(String filename) async {
|
|
final result = await _channel.invokeMethod('sanitizeFilename', {
|
|
'filename': filename,
|
|
});
|
|
return result as String;
|
|
}
|
|
|
|
/// Fetch lyrics for a track
|
|
/// [durationMs] is the track duration in milliseconds for better matching
|
|
static Future<Map<String, dynamic>> fetchLyrics(
|
|
String spotifyId,
|
|
String trackName,
|
|
String artistName, {
|
|
int durationMs = 0,
|
|
}) async {
|
|
final result = await _channel.invokeMethod('fetchLyrics', {
|
|
'spotify_id': spotifyId,
|
|
'track_name': trackName,
|
|
'artist_name': artistName,
|
|
'duration_ms': durationMs,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Get lyrics in LRC format
|
|
/// First tries to extract from embedded file, then falls back to internet
|
|
/// [durationMs] is the track duration in milliseconds for better matching
|
|
static Future<String> getLyricsLRC(
|
|
String spotifyId,
|
|
String trackName,
|
|
String artistName, {
|
|
String? filePath,
|
|
int durationMs = 0,
|
|
}) async {
|
|
final result = await _channel.invokeMethod('getLyricsLRC', {
|
|
'spotify_id': spotifyId,
|
|
'track_name': trackName,
|
|
'artist_name': artistName,
|
|
'file_path': filePath ?? '',
|
|
'duration_ms': durationMs,
|
|
});
|
|
return result as String;
|
|
}
|
|
|
|
/// Embed lyrics into an existing FLAC file
|
|
static Future<Map<String, dynamic>> embedLyricsToFile(
|
|
String filePath,
|
|
String lyrics,
|
|
) async {
|
|
final result = await _channel.invokeMethod('embedLyricsToFile', {
|
|
'file_path': filePath,
|
|
'lyrics': lyrics,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Cleanup idle HTTP connections to prevent TCP exhaustion
|
|
/// Call this periodically during large batch downloads
|
|
static Future<void> cleanupConnections() async {
|
|
await _channel.invokeMethod('cleanupConnections');
|
|
}
|
|
|
|
/// Read metadata directly from a FLAC file
|
|
/// Returns all embedded metadata (title, artist, album, track number, etc.)
|
|
/// This reads from the actual file, not from cached/database data
|
|
static Future<Map<String, dynamic>> readFileMetadata(String filePath) async {
|
|
final result = await _channel.invokeMethod('readFileMetadata', {
|
|
'file_path': filePath,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Start foreground download service to keep downloads running in background
|
|
static Future<void> startDownloadService({
|
|
String trackName = '',
|
|
String artistName = '',
|
|
int queueCount = 0,
|
|
}) async {
|
|
await _channel.invokeMethod('startDownloadService', {
|
|
'track_name': trackName,
|
|
'artist_name': artistName,
|
|
'queue_count': queueCount,
|
|
});
|
|
}
|
|
|
|
/// Stop foreground download service
|
|
static Future<void> stopDownloadService() async {
|
|
await _channel.invokeMethod('stopDownloadService');
|
|
}
|
|
|
|
/// Update download service notification progress
|
|
static Future<void> updateDownloadServiceProgress({
|
|
required String trackName,
|
|
required String artistName,
|
|
required int progress,
|
|
required int total,
|
|
required int queueCount,
|
|
}) async {
|
|
await _channel.invokeMethod('updateDownloadServiceProgress', {
|
|
'track_name': trackName,
|
|
'artist_name': artistName,
|
|
'progress': progress,
|
|
'total': total,
|
|
'queue_count': queueCount,
|
|
});
|
|
}
|
|
|
|
/// Check if download service is running
|
|
static Future<bool> isDownloadServiceRunning() async {
|
|
final result = await _channel.invokeMethod('isDownloadServiceRunning');
|
|
return result as bool;
|
|
}
|
|
|
|
/// Set custom Spotify API credentials
|
|
static Future<void> setSpotifyCredentials(String clientId, String clientSecret) async {
|
|
await _channel.invokeMethod('setSpotifyCredentials', {
|
|
'client_id': clientId,
|
|
'client_secret': clientSecret,
|
|
});
|
|
}
|
|
|
|
/// Check if Spotify credentials are configured
|
|
/// Returns true if credentials are available (custom or env vars)
|
|
static Future<bool> hasSpotifyCredentials() async {
|
|
final result = await _channel.invokeMethod('hasSpotifyCredentials');
|
|
return result as bool;
|
|
}
|
|
|
|
/// Pre-warm track ID cache for album/playlist tracks
|
|
/// This runs in background and returns immediately
|
|
/// Speeds up subsequent downloads by caching ISRC → Track ID mappings
|
|
static Future<void> preWarmTrackCache(List<Map<String, String>> tracks) async {
|
|
final tracksJson = jsonEncode(tracks);
|
|
await _channel.invokeMethod('preWarmTrackCache', {'tracks': tracksJson});
|
|
}
|
|
|
|
/// Get current track cache size
|
|
static Future<int> getTrackCacheSize() async {
|
|
final result = await _channel.invokeMethod('getTrackCacheSize');
|
|
return result as int;
|
|
}
|
|
|
|
/// Clear track ID cache
|
|
static Future<void> clearTrackCache() async {
|
|
await _channel.invokeMethod('clearTrackCache');
|
|
}
|
|
|
|
// ==================== DEEZER API ====================
|
|
|
|
/// Search Deezer for tracks and artists (no API key required)
|
|
static Future<Map<String, dynamic>> searchDeezerAll(String query, {int trackLimit = 15, int artistLimit = 3}) async {
|
|
final result = await _channel.invokeMethod('searchDeezerAll', {
|
|
'query': query,
|
|
'track_limit': trackLimit,
|
|
'artist_limit': artistLimit,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Get Deezer metadata by type and ID
|
|
static Future<Map<String, dynamic>> getDeezerMetadata(String resourceType, String resourceId) async {
|
|
final result = await _channel.invokeMethod('getDeezerMetadata', {
|
|
'resource_type': resourceType,
|
|
'resource_id': resourceId,
|
|
});
|
|
if (result == null) {
|
|
throw Exception('getDeezerMetadata returned null for $resourceType:$resourceId');
|
|
}
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Parse Deezer URL and return type and ID
|
|
static Future<Map<String, dynamic>> parseDeezerUrl(String url) async {
|
|
final result = await _channel.invokeMethod('parseDeezerUrl', {'url': url});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Search Deezer by ISRC
|
|
static Future<Map<String, dynamic>> searchDeezerByISRC(String isrc) async {
|
|
final result = await _channel.invokeMethod('searchDeezerByISRC', {'isrc': isrc});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Convert Spotify track to Deezer and get metadata (for rate limit fallback)
|
|
static Future<Map<String, dynamic>> convertSpotifyToDeezer(String resourceType, String spotifyId) async {
|
|
final result = await _channel.invokeMethod('convertSpotifyToDeezer', {
|
|
'resource_type': resourceType,
|
|
'spotify_id': spotifyId,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Get Spotify metadata with automatic Deezer fallback on rate limit
|
|
static Future<Map<String, dynamic>> getSpotifyMetadataWithFallback(String url) async {
|
|
final result = await _channel.invokeMethod('getSpotifyMetadataWithFallback', {'url': url});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
// ==================== GO BACKEND LOGS ====================
|
|
|
|
/// Get all logs from Go backend
|
|
static Future<List<Map<String, dynamic>>> getGoLogs() async {
|
|
final result = await _channel.invokeMethod('getLogs');
|
|
final logs = jsonDecode(result as String) as List<dynamic>;
|
|
return logs.map((e) => e as Map<String, dynamic>).toList();
|
|
}
|
|
|
|
/// Get logs since a specific index (for incremental updates)
|
|
static Future<Map<String, dynamic>> getGoLogsSince(int index) async {
|
|
final result = await _channel.invokeMethod('getLogsSince', {'index': index});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Clear Go backend logs
|
|
static Future<void> clearGoLogs() async {
|
|
await _channel.invokeMethod('clearLogs');
|
|
}
|
|
|
|
/// Get Go backend log count
|
|
static Future<int> getGoLogCount() async {
|
|
final result = await _channel.invokeMethod('getLogCount');
|
|
return result as int;
|
|
}
|
|
|
|
/// Enable or disable Go backend logging
|
|
static Future<void> setGoLoggingEnabled(bool enabled) async {
|
|
await _channel.invokeMethod('setLoggingEnabled', {'enabled': enabled});
|
|
}
|
|
|
|
// ==================== EXTENSION SYSTEM ====================
|
|
|
|
/// Initialize the extension system
|
|
static Future<void> initExtensionSystem(String extensionsDir, String dataDir) async {
|
|
_log.d('initExtensionSystem: $extensionsDir, $dataDir');
|
|
await _channel.invokeMethod('initExtensionSystem', {
|
|
'extensions_dir': extensionsDir,
|
|
'data_dir': dataDir,
|
|
});
|
|
}
|
|
|
|
/// Load all extensions from directory
|
|
static Future<Map<String, dynamic>> loadExtensionsFromDir(String dirPath) async {
|
|
_log.d('loadExtensionsFromDir: $dirPath');
|
|
final result = await _channel.invokeMethod('loadExtensionsFromDir', {
|
|
'dir_path': dirPath,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Load a single extension from file
|
|
static Future<Map<String, dynamic>> loadExtensionFromPath(String filePath) async {
|
|
_log.d('loadExtensionFromPath: $filePath');
|
|
final result = await _channel.invokeMethod('loadExtensionFromPath', {
|
|
'file_path': filePath,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Unload an extension
|
|
static Future<void> unloadExtension(String extensionId) async {
|
|
_log.d('unloadExtension: $extensionId');
|
|
await _channel.invokeMethod('unloadExtension', {
|
|
'extension_id': extensionId,
|
|
});
|
|
}
|
|
|
|
/// Remove an extension completely (unload + delete files)
|
|
static Future<void> removeExtension(String extensionId) async {
|
|
_log.d('removeExtension: $extensionId');
|
|
await _channel.invokeMethod('removeExtension', {
|
|
'extension_id': extensionId,
|
|
});
|
|
}
|
|
|
|
/// Upgrade an existing extension from a new package file
|
|
static Future<Map<String, dynamic>> upgradeExtension(String filePath) async {
|
|
_log.d('upgradeExtension: $filePath');
|
|
final result = await _channel.invokeMethod('upgradeExtension', {
|
|
'file_path': filePath,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Check if a package file is an upgrade for an existing extension
|
|
static Future<Map<String, dynamic>> checkExtensionUpgrade(String filePath) async {
|
|
_log.d('checkExtensionUpgrade: $filePath');
|
|
final result = await _channel.invokeMethod('checkExtensionUpgrade', {
|
|
'file_path': filePath,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Get all installed extensions
|
|
static Future<List<Map<String, dynamic>>> getInstalledExtensions() async {
|
|
final result = await _channel.invokeMethod('getInstalledExtensions');
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as Map<String, dynamic>).toList();
|
|
}
|
|
|
|
/// Enable or disable an extension
|
|
static Future<void> setExtensionEnabled(String extensionId, bool enabled) async {
|
|
_log.d('setExtensionEnabled: $extensionId = $enabled');
|
|
await _channel.invokeMethod('setExtensionEnabled', {
|
|
'extension_id': extensionId,
|
|
'enabled': enabled,
|
|
});
|
|
}
|
|
|
|
/// Set provider priority order
|
|
static Future<void> setProviderPriority(List<String> providerIds) async {
|
|
_log.d('setProviderPriority: $providerIds');
|
|
await _channel.invokeMethod('setProviderPriority', {
|
|
'priority': jsonEncode(providerIds),
|
|
});
|
|
}
|
|
|
|
/// Get provider priority order
|
|
static Future<List<String>> getProviderPriority() async {
|
|
final result = await _channel.invokeMethod('getProviderPriority');
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as String).toList();
|
|
}
|
|
|
|
/// Set metadata provider priority order
|
|
static Future<void> setMetadataProviderPriority(List<String> providerIds) async {
|
|
_log.d('setMetadataProviderPriority: $providerIds');
|
|
await _channel.invokeMethod('setMetadataProviderPriority', {
|
|
'priority': jsonEncode(providerIds),
|
|
});
|
|
}
|
|
|
|
/// Get metadata provider priority order
|
|
static Future<List<String>> getMetadataProviderPriority() async {
|
|
final result = await _channel.invokeMethod('getMetadataProviderPriority');
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as String).toList();
|
|
}
|
|
|
|
/// Get extension settings
|
|
static Future<Map<String, dynamic>> getExtensionSettings(String extensionId) async {
|
|
final result = await _channel.invokeMethod('getExtensionSettings', {
|
|
'extension_id': extensionId,
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Set extension settings
|
|
static Future<void> setExtensionSettings(String extensionId, Map<String, dynamic> settings) async {
|
|
_log.d('setExtensionSettings: $extensionId');
|
|
await _channel.invokeMethod('setExtensionSettings', {
|
|
'extension_id': extensionId,
|
|
'settings': jsonEncode(settings),
|
|
});
|
|
}
|
|
|
|
/// Search tracks using extension providers
|
|
static Future<List<Map<String, dynamic>>> searchTracksWithExtensions(String query, {int limit = 20}) async {
|
|
_log.d('searchTracksWithExtensions: "$query"');
|
|
final result = await _channel.invokeMethod('searchTracksWithExtensions', {
|
|
'query': query,
|
|
'limit': limit,
|
|
});
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as Map<String, dynamic>).toList();
|
|
}
|
|
|
|
/// Download with extension providers (includes fallback)
|
|
static Future<Map<String, dynamic>> downloadWithExtensions({
|
|
required String isrc,
|
|
required String spotifyId,
|
|
required String trackName,
|
|
required String artistName,
|
|
required String albumName,
|
|
String? albumArtist,
|
|
String? coverUrl,
|
|
required String outputDir,
|
|
required String filenameFormat,
|
|
String quality = 'LOSSLESS',
|
|
bool embedLyrics = true,
|
|
bool embedMaxQualityCover = true,
|
|
int trackNumber = 1,
|
|
int discNumber = 1,
|
|
int totalTracks = 1,
|
|
String? releaseDate,
|
|
String? itemId,
|
|
int durationMs = 0,
|
|
String? source, // Extension ID that provided this track (prioritize this extension)
|
|
}) async {
|
|
_log.i('downloadWithExtensions: "$trackName" by $artistName${source != null ? ' (source: $source)' : ''}');
|
|
final request = jsonEncode({
|
|
'isrc': isrc,
|
|
'spotify_id': spotifyId,
|
|
'track_name': trackName,
|
|
'artist_name': artistName,
|
|
'album_name': albumName,
|
|
'album_artist': albumArtist ?? artistName,
|
|
'cover_url': coverUrl,
|
|
'output_dir': outputDir,
|
|
'filename_format': filenameFormat,
|
|
'quality': quality,
|
|
'embed_lyrics': embedLyrics,
|
|
'embed_max_quality_cover': embedMaxQualityCover,
|
|
'track_number': trackNumber,
|
|
'disc_number': discNumber,
|
|
'total_tracks': totalTracks,
|
|
'release_date': releaseDate ?? '',
|
|
'item_id': itemId ?? '',
|
|
'duration_ms': durationMs,
|
|
'source': source ?? '', // Extension ID that provided this track
|
|
});
|
|
|
|
final result = await _channel.invokeMethod('downloadWithExtensions', request);
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Cleanup all extensions (call on app close)
|
|
static Future<void> cleanupExtensions() async {
|
|
_log.d('cleanupExtensions');
|
|
await _channel.invokeMethod('cleanupExtensions');
|
|
}
|
|
|
|
// ==================== EXTENSION AUTH API ====================
|
|
|
|
/// Get pending auth request for an extension (if any)
|
|
static Future<Map<String, dynamic>?> getExtensionPendingAuth(String extensionId) async {
|
|
final result = await _channel.invokeMethod('getExtensionPendingAuth', {
|
|
'extension_id': extensionId,
|
|
});
|
|
if (result == null) return null;
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Set auth code for an extension (after OAuth callback)
|
|
static Future<void> setExtensionAuthCode(String extensionId, String authCode) async {
|
|
_log.d('setExtensionAuthCode: $extensionId');
|
|
await _channel.invokeMethod('setExtensionAuthCode', {
|
|
'extension_id': extensionId,
|
|
'auth_code': authCode,
|
|
});
|
|
}
|
|
|
|
/// Set tokens for an extension (after token exchange)
|
|
static Future<void> setExtensionTokens(
|
|
String extensionId, {
|
|
required String accessToken,
|
|
String? refreshToken,
|
|
int? expiresIn,
|
|
}) async {
|
|
_log.d('setExtensionTokens: $extensionId');
|
|
await _channel.invokeMethod('setExtensionTokens', {
|
|
'extension_id': extensionId,
|
|
'access_token': accessToken,
|
|
'refresh_token': refreshToken ?? '',
|
|
'expires_in': expiresIn ?? 0,
|
|
});
|
|
}
|
|
|
|
/// Clear pending auth request for an extension
|
|
static Future<void> clearExtensionPendingAuth(String extensionId) async {
|
|
await _channel.invokeMethod('clearExtensionPendingAuth', {
|
|
'extension_id': extensionId,
|
|
});
|
|
}
|
|
|
|
/// Check if extension is authenticated
|
|
static Future<bool> isExtensionAuthenticated(String extensionId) async {
|
|
final result = await _channel.invokeMethod('isExtensionAuthenticated', {
|
|
'extension_id': extensionId,
|
|
});
|
|
return result as bool;
|
|
}
|
|
|
|
/// Get all pending auth requests (for polling)
|
|
static Future<List<Map<String, dynamic>>> getAllPendingAuthRequests() async {
|
|
final result = await _channel.invokeMethod('getAllPendingAuthRequests');
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as Map<String, dynamic>).toList();
|
|
}
|
|
|
|
// ==================== EXTENSION FFMPEG API ====================
|
|
|
|
/// Get pending FFmpeg command for execution
|
|
static Future<Map<String, dynamic>?> getPendingFFmpegCommand(String commandId) async {
|
|
final result = await _channel.invokeMethod('getPendingFFmpegCommand', {
|
|
'command_id': commandId,
|
|
});
|
|
if (result == null) return null;
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Set FFmpeg command result
|
|
static Future<void> setFFmpegCommandResult(
|
|
String commandId, {
|
|
required bool success,
|
|
String output = '',
|
|
String error = '',
|
|
}) async {
|
|
await _channel.invokeMethod('setFFmpegCommandResult', {
|
|
'command_id': commandId,
|
|
'success': success,
|
|
'output': output,
|
|
'error': error,
|
|
});
|
|
}
|
|
|
|
/// Get all pending FFmpeg commands
|
|
static Future<List<Map<String, dynamic>>> getAllPendingFFmpegCommands() async {
|
|
final result = await _channel.invokeMethod('getAllPendingFFmpegCommands');
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as Map<String, dynamic>).toList();
|
|
}
|
|
|
|
// ==================== EXTENSION CUSTOM SEARCH ====================
|
|
|
|
/// Perform custom search using an extension
|
|
static Future<List<Map<String, dynamic>>> customSearchWithExtension(
|
|
String extensionId,
|
|
String query, {
|
|
Map<String, dynamic>? options,
|
|
}) async {
|
|
final result = await _channel.invokeMethod('customSearchWithExtension', {
|
|
'extension_id': extensionId,
|
|
'query': query,
|
|
'options': options != null ? jsonEncode(options) : '',
|
|
});
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as Map<String, dynamic>).toList();
|
|
}
|
|
|
|
/// Get all extensions that provide custom search
|
|
static Future<List<Map<String, dynamic>>> getSearchProviders() async {
|
|
final result = await _channel.invokeMethod('getSearchProviders');
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as Map<String, dynamic>).toList();
|
|
}
|
|
|
|
// ==================== EXTENSION URL HANDLER ====================
|
|
|
|
/// Handle a URL with any matching extension
|
|
/// Returns null if no extension can handle the URL
|
|
static Future<Map<String, dynamic>?> handleURLWithExtension(String url) async {
|
|
try {
|
|
final result = await _channel.invokeMethod('handleURLWithExtension', {
|
|
'url': url,
|
|
});
|
|
if (result == null || result == '') return null;
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Find an extension that can handle the given URL
|
|
/// Returns extension ID or null if none found
|
|
static Future<String?> findURLHandler(String url) async {
|
|
final result = await _channel.invokeMethod('findURLHandler', {
|
|
'url': url,
|
|
});
|
|
if (result == null || result == '') return null;
|
|
return result as String;
|
|
}
|
|
|
|
/// Get all extensions that handle custom URLs
|
|
static Future<List<Map<String, dynamic>>> getURLHandlers() async {
|
|
final result = await _channel.invokeMethod('getURLHandlers');
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as Map<String, dynamic>).toList();
|
|
}
|
|
|
|
/// Get album tracks using an extension
|
|
static Future<Map<String, dynamic>?> getAlbumWithExtension(
|
|
String extensionId,
|
|
String albumId,
|
|
) async {
|
|
try {
|
|
final result = await _channel.invokeMethod('getAlbumWithExtension', {
|
|
'extension_id': extensionId,
|
|
'album_id': albumId,
|
|
});
|
|
if (result == null || result == '') return null;
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
} catch (e) {
|
|
_log.e('getAlbumWithExtension failed: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Get playlist tracks using an extension
|
|
static Future<Map<String, dynamic>?> getPlaylistWithExtension(
|
|
String extensionId,
|
|
String playlistId,
|
|
) async {
|
|
try {
|
|
final result = await _channel.invokeMethod('getPlaylistWithExtension', {
|
|
'extension_id': extensionId,
|
|
'playlist_id': playlistId,
|
|
});
|
|
if (result == null || result == '') return null;
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
} catch (e) {
|
|
_log.e('getPlaylistWithExtension failed: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Get artist info and albums using an extension
|
|
static Future<Map<String, dynamic>?> getArtistWithExtension(
|
|
String extensionId,
|
|
String artistId,
|
|
) async {
|
|
try {
|
|
final result = await _channel.invokeMethod('getArtistWithExtension', {
|
|
'extension_id': extensionId,
|
|
'artist_id': artistId,
|
|
});
|
|
if (result == null || result == '') return null;
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
} catch (e) {
|
|
_log.e('getArtistWithExtension failed: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ==================== EXTENSION POST-PROCESSING ====================
|
|
|
|
/// Run post-processing hooks on a file
|
|
static Future<Map<String, dynamic>> runPostProcessing(
|
|
String filePath, {
|
|
Map<String, dynamic>? metadata,
|
|
}) async {
|
|
final result = await _channel.invokeMethod('runPostProcessing', {
|
|
'file_path': filePath,
|
|
'metadata': metadata != null ? jsonEncode(metadata) : '',
|
|
});
|
|
return jsonDecode(result as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Get all extensions that provide post-processing
|
|
static Future<List<Map<String, dynamic>>> getPostProcessingProviders() async {
|
|
final result = await _channel.invokeMethod('getPostProcessingProviders');
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as Map<String, dynamic>).toList();
|
|
}
|
|
|
|
// ==================== EXTENSION STORE ====================
|
|
|
|
/// Initialize extension store
|
|
static Future<void> initExtensionStore(String cacheDir) async {
|
|
_log.d('initExtensionStore: $cacheDir');
|
|
await _channel.invokeMethod('initExtensionStore', {'cache_dir': cacheDir});
|
|
}
|
|
|
|
/// Get all extensions from store with installation status
|
|
static Future<List<Map<String, dynamic>>> getStoreExtensions({bool forceRefresh = false}) async {
|
|
_log.d('getStoreExtensions (forceRefresh: $forceRefresh)');
|
|
final result = await _channel.invokeMethod('getStoreExtensions', {
|
|
'force_refresh': forceRefresh,
|
|
});
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as Map<String, dynamic>).toList();
|
|
}
|
|
|
|
/// Search extensions in store
|
|
static Future<List<Map<String, dynamic>>> searchStoreExtensions(String query, {String? category}) async {
|
|
_log.d('searchStoreExtensions: "$query" (category: $category)');
|
|
final result = await _channel.invokeMethod('searchStoreExtensions', {
|
|
'query': query,
|
|
'category': category ?? '',
|
|
});
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.map((e) => e as Map<String, dynamic>).toList();
|
|
}
|
|
|
|
/// Get store categories
|
|
static Future<List<String>> getStoreCategories() async {
|
|
final result = await _channel.invokeMethod('getStoreCategories');
|
|
final list = jsonDecode(result as String) as List<dynamic>;
|
|
return list.cast<String>();
|
|
}
|
|
|
|
/// Download extension from store
|
|
static Future<String> downloadStoreExtension(String extensionId, String destDir) async {
|
|
_log.i('downloadStoreExtension: $extensionId to $destDir');
|
|
final result = await _channel.invokeMethod('downloadStoreExtension', {
|
|
'extension_id': extensionId,
|
|
'dest_dir': destDir,
|
|
});
|
|
return result as String;
|
|
}
|
|
|
|
/// Clear store cache
|
|
static Future<void> clearStoreCache() async {
|
|
_log.d('clearStoreCache');
|
|
await _channel.invokeMethod('clearStoreCache');
|
|
}
|
|
}
|