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> parseSpotifyUrl(String url) async { _log.d('parseSpotifyUrl: $url'); final result = await _channel.invokeMethod('parseSpotifyUrl', {'url': url}); return jsonDecode(result as String) as Map; } /// Get Spotify metadata from URL static Future> getSpotifyMetadata(String url) async { _log.d('getSpotifyMetadata: $url'); final result = await _channel.invokeMethod('getSpotifyMetadata', {'url': url}); return jsonDecode(result as String) as Map; } /// Search Spotify static Future> 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; } /// Search Spotify for tracks and artists static Future> 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; } /// Check track availability on streaming services static Future> 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; } /// Download a track from specific service static Future> 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; 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> 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, // Extended metadata for FLAC tagging String? genre, String? label, String? copyright, }) 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, // Extended metadata 'genre': genre ?? '', 'label': label ?? '', 'copyright': copyright ?? '', }); final result = await _channel.invokeMethod('downloadWithFallback', request); final response = jsonDecode(result as String) as Map; 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> getDownloadProgress() async { final result = await _channel.invokeMethod('getDownloadProgress'); return jsonDecode(result as String) as Map; } /// Get progress for all active downloads (concurrent mode) static Future> getAllDownloadProgress() async { final result = await _channel.invokeMethod('getAllDownloadProgress'); return jsonDecode(result as String) as Map; } /// Initialize progress tracking for a download item static Future initItemProgress(String itemId) async { await _channel.invokeMethod('initItemProgress', {'item_id': itemId}); } /// Finish progress tracking for a download item static Future finishItemProgress(String itemId) async { await _channel.invokeMethod('finishItemProgress', {'item_id': itemId}); } /// Clear progress tracking for a download item static Future clearItemProgress(String itemId) async { await _channel.invokeMethod('clearItemProgress', {'item_id': itemId}); } /// Cancel an in-progress download static Future cancelDownload(String itemId) async { await _channel.invokeMethod('cancelDownload', {'item_id': itemId}); } /// Set download directory static Future setDownloadDirectory(String path) async { await _channel.invokeMethod('setDownloadDirectory', {'path': path}); } /// Check if file with ISRC already exists static Future> checkDuplicate(String outputDir, String isrc) async { final result = await _channel.invokeMethod('checkDuplicate', { 'output_dir': outputDir, 'isrc': isrc, }); return jsonDecode(result as String) as Map; } /// Build filename from template static Future buildFilename(String template, Map metadata) async { final result = await _channel.invokeMethod('buildFilename', { 'template': template, 'metadata': jsonEncode(metadata), }); return result as String; } /// Sanitize filename static Future 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> 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; } /// 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 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> embedLyricsToFile( String filePath, String lyrics, ) async { final result = await _channel.invokeMethod('embedLyricsToFile', { 'file_path': filePath, 'lyrics': lyrics, }); return jsonDecode(result as String) as Map; } /// Cleanup idle HTTP connections to prevent TCP exhaustion /// Call this periodically during large batch downloads static Future 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> readFileMetadata(String filePath) async { final result = await _channel.invokeMethod('readFileMetadata', { 'file_path': filePath, }); return jsonDecode(result as String) as Map; } /// Start foreground download service to keep downloads running in background static Future 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 stopDownloadService() async { await _channel.invokeMethod('stopDownloadService'); } /// Update download service notification progress static Future 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 isDownloadServiceRunning() async { final result = await _channel.invokeMethod('isDownloadServiceRunning'); return result as bool; } /// Set custom Spotify API credentials static Future 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 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 preWarmTrackCache(List> tracks) async { final tracksJson = jsonEncode(tracks); await _channel.invokeMethod('preWarmTrackCache', {'tracks': tracksJson}); } /// Get current track cache size static Future getTrackCacheSize() async { final result = await _channel.invokeMethod('getTrackCacheSize'); return result as int; } /// Clear track ID cache static Future clearTrackCache() async { await _channel.invokeMethod('clearTrackCache'); } // ==================== DEEZER API ==================== /// Search Deezer for tracks and artists (no API key required) static Future> 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; } /// Get Deezer metadata by type and ID static Future> 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; } /// Parse Deezer URL and return type and ID static Future> parseDeezerUrl(String url) async { final result = await _channel.invokeMethod('parseDeezerUrl', {'url': url}); return jsonDecode(result as String) as Map; } /// Search Deezer by ISRC static Future> searchDeezerByISRC(String isrc) async { final result = await _channel.invokeMethod('searchDeezerByISRC', {'isrc': isrc}); return jsonDecode(result as String) as Map; } /// Get extended metadata (genre, label) from Deezer using track ID /// Returns {"genre": "...", "label": "..."} or null if not found static Future?> getDeezerExtendedMetadata(String trackId) async { try { final result = await _channel.invokeMethod('getDeezerExtendedMetadata', { 'track_id': trackId, }); if (result == null) return null; final data = jsonDecode(result as String) as Map; return { 'genre': data['genre'] as String? ?? '', 'label': data['label'] as String? ?? '', }; } catch (e) { _log.w('Failed to get Deezer extended metadata for $trackId: $e'); return null; } } /// Convert Spotify track to Deezer and get metadata (for rate limit fallback) static Future> 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; } /// Get Spotify metadata with automatic Deezer fallback on rate limit static Future> getSpotifyMetadataWithFallback(String url) async { final result = await _channel.invokeMethod('getSpotifyMetadataWithFallback', {'url': url}); return jsonDecode(result as String) as Map; } // ==================== GO BACKEND LOGS ==================== /// Get all logs from Go backend static Future>> getGoLogs() async { final result = await _channel.invokeMethod('getLogs'); final logs = jsonDecode(result as String) as List; return logs.map((e) => e as Map).toList(); } /// Get logs since a specific index (for incremental updates) static Future> getGoLogsSince(int index) async { final result = await _channel.invokeMethod('getLogsSince', {'index': index}); return jsonDecode(result as String) as Map; } /// Clear Go backend logs static Future clearGoLogs() async { await _channel.invokeMethod('clearLogs'); } /// Get Go backend log count static Future getGoLogCount() async { final result = await _channel.invokeMethod('getLogCount'); return result as int; } /// Enable or disable Go backend logging static Future setGoLoggingEnabled(bool enabled) async { await _channel.invokeMethod('setLoggingEnabled', {'enabled': enabled}); } // ==================== EXTENSION SYSTEM ==================== /// Initialize the extension system static Future 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> loadExtensionsFromDir(String dirPath) async { _log.d('loadExtensionsFromDir: $dirPath'); final result = await _channel.invokeMethod('loadExtensionsFromDir', { 'dir_path': dirPath, }); return jsonDecode(result as String) as Map; } /// Load a single extension from file static Future> loadExtensionFromPath(String filePath) async { _log.d('loadExtensionFromPath: $filePath'); final result = await _channel.invokeMethod('loadExtensionFromPath', { 'file_path': filePath, }); return jsonDecode(result as String) as Map; } /// Unload an extension static Future unloadExtension(String extensionId) async { _log.d('unloadExtension: $extensionId'); await _channel.invokeMethod('unloadExtension', { 'extension_id': extensionId, }); } /// Remove an extension completely (unload + delete files) static Future 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> upgradeExtension(String filePath) async { _log.d('upgradeExtension: $filePath'); final result = await _channel.invokeMethod('upgradeExtension', { 'file_path': filePath, }); return jsonDecode(result as String) as Map; } /// Check if a package file is an upgrade for an existing extension static Future> checkExtensionUpgrade(String filePath) async { _log.d('checkExtensionUpgrade: $filePath'); final result = await _channel.invokeMethod('checkExtensionUpgrade', { 'file_path': filePath, }); return jsonDecode(result as String) as Map; } /// Get all installed extensions static Future>> getInstalledExtensions() async { final result = await _channel.invokeMethod('getInstalledExtensions'); final list = jsonDecode(result as String) as List; return list.map((e) => e as Map).toList(); } /// Enable or disable an extension static Future 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 setProviderPriority(List providerIds) async { _log.d('setProviderPriority: $providerIds'); await _channel.invokeMethod('setProviderPriority', { 'priority': jsonEncode(providerIds), }); } /// Get provider priority order static Future> getProviderPriority() async { final result = await _channel.invokeMethod('getProviderPriority'); final list = jsonDecode(result as String) as List; return list.map((e) => e as String).toList(); } /// Set metadata provider priority order static Future setMetadataProviderPriority(List providerIds) async { _log.d('setMetadataProviderPriority: $providerIds'); await _channel.invokeMethod('setMetadataProviderPriority', { 'priority': jsonEncode(providerIds), }); } /// Get metadata provider priority order static Future> getMetadataProviderPriority() async { final result = await _channel.invokeMethod('getMetadataProviderPriority'); final list = jsonDecode(result as String) as List; return list.map((e) => e as String).toList(); } /// Get extension settings static Future> getExtensionSettings(String extensionId) async { final result = await _channel.invokeMethod('getExtensionSettings', { 'extension_id': extensionId, }); return jsonDecode(result as String) as Map; } /// Set extension settings static Future setExtensionSettings(String extensionId, Map settings) async { _log.d('setExtensionSettings: $extensionId'); await _channel.invokeMethod('setExtensionSettings', { 'extension_id': extensionId, 'settings': jsonEncode(settings), }); } /// Invoke an action on an extension (e.g., button click handler like "startLogin") /// Returns the result from the JS function static Future> invokeExtensionAction(String extensionId, String actionName) async { _log.d('invokeExtensionAction: $extensionId.$actionName'); final result = await _channel.invokeMethod('invokeExtensionAction', { 'extension_id': extensionId, 'action': actionName, }); if (result == null || (result as String).isEmpty) { return {'success': true}; } return jsonDecode(result) as Map; } /// Search tracks using extension providers static Future>> 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; return list.map((e) => e as Map).toList(); } /// Download with extension providers (includes fallback) static Future> 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) String? genre, String? label, }) 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 'genre': genre ?? '', 'label': label ?? '', }); final result = await _channel.invokeMethod('downloadWithExtensions', request); return jsonDecode(result as String) as Map; } /// Cleanup all extensions (call on app close) static Future cleanupExtensions() async { _log.d('cleanupExtensions'); await _channel.invokeMethod('cleanupExtensions'); } // ==================== EXTENSION AUTH API ==================== /// Get pending auth request for an extension (if any) static Future?> 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; } /// Set auth code for an extension (after OAuth callback) static Future 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 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 clearExtensionPendingAuth(String extensionId) async { await _channel.invokeMethod('clearExtensionPendingAuth', { 'extension_id': extensionId, }); } /// Check if extension is authenticated static Future 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>> getAllPendingAuthRequests() async { final result = await _channel.invokeMethod('getAllPendingAuthRequests'); final list = jsonDecode(result as String) as List; return list.map((e) => e as Map).toList(); } // ==================== EXTENSION FFMPEG API ==================== /// Get pending FFmpeg command for execution static Future?> 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; } /// Set FFmpeg command result static Future 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>> getAllPendingFFmpegCommands() async { final result = await _channel.invokeMethod('getAllPendingFFmpegCommands'); final list = jsonDecode(result as String) as List; return list.map((e) => e as Map).toList(); } // ==================== EXTENSION CUSTOM SEARCH ==================== /// Perform custom search using an extension static Future>> customSearchWithExtension( String extensionId, String query, { Map? 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; return list.map((e) => e as Map).toList(); } /// Get all extensions that provide custom search static Future>> getSearchProviders() async { final result = await _channel.invokeMethod('getSearchProviders'); final list = jsonDecode(result as String) as List; return list.map((e) => e as Map).toList(); } // ==================== EXTENSION URL HANDLER ==================== /// Handle a URL with any matching extension /// Returns null if no extension can handle the URL static Future?> 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; } catch (e) { return null; } } /// Find an extension that can handle the given URL /// Returns extension ID or null if none found static Future 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>> getURLHandlers() async { final result = await _channel.invokeMethod('getURLHandlers'); final list = jsonDecode(result as String) as List; return list.map((e) => e as Map).toList(); } /// Get album tracks using an extension static Future?> 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; } catch (e) { _log.e('getAlbumWithExtension failed: $e'); return null; } } /// Get playlist tracks using an extension static Future?> 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; } catch (e) { _log.e('getPlaylistWithExtension failed: $e'); return null; } } /// Get artist info and albums using an extension static Future?> 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; } catch (e) { _log.e('getArtistWithExtension failed: $e'); return null; } } // ==================== EXTENSION POST-PROCESSING ==================== /// Run post-processing hooks on a file static Future> runPostProcessing( String filePath, { Map? metadata, }) async { final result = await _channel.invokeMethod('runPostProcessing', { 'file_path': filePath, 'metadata': metadata != null ? jsonEncode(metadata) : '', }); return jsonDecode(result as String) as Map; } /// Get all extensions that provide post-processing static Future>> getPostProcessingProviders() async { final result = await _channel.invokeMethod('getPostProcessingProviders'); final list = jsonDecode(result as String) as List; return list.map((e) => e as Map).toList(); } // ==================== EXTENSION STORE ==================== /// Initialize extension store static Future 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>> 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; return list.map((e) => e as Map).toList(); } /// Search extensions in store static Future>> 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; return list.map((e) => e as Map).toList(); } /// Get store categories static Future> getStoreCategories() async { final result = await _channel.invokeMethod('getStoreCategories'); final list = jsonDecode(result as String) as List; return list.cast(); } /// Download extension from store static Future 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 clearStoreCache() async { _log.d('clearStoreCache'); await _channel.invokeMethod('clearStoreCache'); } }