Files
SpotiFLAC-Mobile/lib/services/platform_bridge.dart
T
2026-05-03 14:12:53 +07:00

1847 lines
57 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotiflac_android/services/download_request_payload.dart';
import 'package:spotiflac_android/utils/logger.dart';
final _log = AppLogger('PlatformBridge');
class _BridgeCacheEntry {
final Map<String, dynamic> value;
final DateTime expiresAt;
const _BridgeCacheEntry({required this.value, required this.expiresAt});
bool get isExpired => DateTime.now().isAfter(expiresAt);
}
class _BridgeInFlight<T> {
final String requestId;
final String scopeKey;
final Future<T> future;
const _BridgeInFlight({
required this.requestId,
required this.scopeKey,
required this.future,
});
}
class PlatformBridge {
static const _channel = MethodChannel('com.zarz.spotiflac/backend');
static const _jsonResultFileKey = '__json_file';
static const _metadataCacheTtl = Duration(minutes: 20);
static const _availabilityCacheTtl = Duration(minutes: 15);
static const _bridgeCacheMaxEntries = 256;
static const _metadataPersistentCacheKey = 'bridge_metadata_lookup_cache_v1';
static const _availabilityPersistentCacheKey =
'bridge_availability_lookup_cache_v1';
static const _downloadProgressEvents = EventChannel(
'com.zarz.spotiflac/download_progress_stream',
);
static const _libraryScanProgressEvents = EventChannel(
'com.zarz.spotiflac/library_scan_progress_stream',
);
static final Map<String, _BridgeCacheEntry> _metadataCache = {};
static final Map<String, _BridgeCacheEntry> _availabilityCache = {};
static final Map<String, Future<Map<String, dynamic>>> _metadataInFlight = {};
static final Map<String, Future<Map<String, dynamic>>> _availabilityInFlight =
{};
static final Map<String, _BridgeInFlight<List<Map<String, dynamic>>>>
_customSearchInFlight = {};
static final Map<String, _BridgeInFlight<Map<String, dynamic>?>>
_homeFeedInFlight = {};
static Future<void>? _persistentLookupCacheLoadFuture;
static int _lookupCacheGeneration = 0;
static int _extensionRequestSequence = 0;
static bool get supportsCoreBackend => Platform.isAndroid || Platform.isIOS;
static bool get supportsExtensionSystem =>
Platform.isAndroid || Platform.isIOS;
static Future<Map<String, dynamic>> checkAvailability(
String spotifyId,
String isrc,
) async {
final cacheKey = _availabilityCacheKey(spotifyId, isrc);
if (cacheKey.isEmpty) {
_log.d('checkAvailability: $spotifyId (ISRC: $isrc)');
final result = await _channel.invokeMethod('checkAvailability', {
'spotify_id': spotifyId,
'isrc': isrc,
});
return _decodeRequiredMapResult(result, 'checkAvailability');
}
await _ensurePersistentLookupCachesLoaded();
final cached = _getCachedMap(_availabilityCache, cacheKey);
if (cached != null) return cached;
final inFlight = _availabilityInFlight[cacheKey];
if (inFlight != null) return _copyStringMap(await inFlight);
final generation = _lookupCacheGeneration;
final future = _invokeCachedMap(
cacheKey,
_availabilityCache,
() async {
_log.d('checkAvailability: $spotifyId (ISRC: $isrc)');
final result = await _channel.invokeMethod('checkAvailability', {
'spotify_id': spotifyId,
'isrc': isrc,
});
return _decodeRequiredMapResult(result, 'checkAvailability');
},
_availabilityCacheTtl,
generation,
_availabilityPersistentCacheKey,
);
_availabilityInFlight[cacheKey] = future;
try {
return _copyStringMap(await future);
} finally {
_availabilityInFlight.remove(cacheKey);
}
}
static Future<Map<String, dynamic>> _invokeCachedMap(
String key,
Map<String, _BridgeCacheEntry> cache,
Future<Map<String, dynamic>> Function() loader,
Duration ttl,
int generation,
String persistentCacheKey,
) async {
final value = await loader();
if (generation == _lookupCacheGeneration) {
_putCachedMap(cache, key, value, ttl, persistentCacheKey);
}
return _copyStringMap(value);
}
static String _availabilityCacheKey(String spotifyId, String isrc) {
final normalizedIsrc = isrc.trim().toUpperCase();
if (normalizedIsrc.isNotEmpty) {
return 'isrc:$normalizedIsrc';
}
final normalizedSpotifyId = spotifyId.trim();
if (normalizedSpotifyId.isEmpty) return '';
return 'spotify:$normalizedSpotifyId';
}
static String _providerMetadataCacheKey(
String providerId,
String resourceType,
String resourceId,
) {
return [
providerId.trim().toLowerCase(),
resourceType.trim().toLowerCase(),
resourceId.trim(),
].join(':');
}
static Map<String, dynamic>? _getCachedMap(
Map<String, _BridgeCacheEntry> cache,
String key,
) {
_pruneExpiredBridgeCache(cache);
final entry = cache[key];
if (entry == null) return null;
if (entry.isExpired) {
cache.remove(key);
return null;
}
return _copyStringMap(entry.value);
}
static void _putCachedMap(
Map<String, _BridgeCacheEntry> cache,
String key,
Map<String, dynamic> value,
Duration ttl,
String persistentCacheKey,
) {
_pruneExpiredBridgeCache(cache);
while (cache.length >= _bridgeCacheMaxEntries && cache.isNotEmpty) {
cache.remove(cache.keys.first);
}
cache[key] = _BridgeCacheEntry(
value: _copyStringMap(value),
expiresAt: DateTime.now().add(ttl),
);
unawaited(
_persistLookupCache(cache, persistentCacheKey, _lookupCacheGeneration),
);
}
static void _pruneExpiredBridgeCache(Map<String, _BridgeCacheEntry> cache) {
if (cache.isEmpty) return;
final now = DateTime.now();
cache.removeWhere((_, entry) => now.isAfter(entry.expiresAt));
}
static dynamic _copyJsonLike(dynamic value) {
if (value is Map) {
return <String, dynamic>{
for (final entry in value.entries)
entry.key.toString(): _copyJsonLike(entry.value),
};
}
if (value is List) {
return value.map(_copyJsonLike).toList(growable: false);
}
return value;
}
static Map<String, dynamic> _copyStringMap(Map<String, dynamic> value) {
return <String, dynamic>{
for (final entry in value.entries) entry.key: _copyJsonLike(entry.value),
};
}
static Map<String, dynamic>? _copyNullableStringMap(
Map<String, dynamic>? value,
) {
if (value == null) return null;
return _copyStringMap(value);
}
static List<Map<String, dynamic>> _copyMapList(
List<Map<String, dynamic>> value,
) {
return value.map(_copyStringMap).toList(growable: false);
}
static dynamic _canonicalizeJsonLike(dynamic value) {
if (value is Map) {
final entries = value.entries.toList()
..sort((a, b) => a.key.toString().compareTo(b.key.toString()));
return <String, dynamic>{
for (final entry in entries)
entry.key.toString(): _canonicalizeJsonLike(entry.value),
};
}
if (value is List) {
return value.map(_canonicalizeJsonLike).toList(growable: false);
}
return value;
}
static Future<void> _ensurePersistentLookupCachesLoaded() {
return _persistentLookupCacheLoadFuture ??= _loadPersistentLookupCaches(
_lookupCacheGeneration,
);
}
static Future<void> _loadPersistentLookupCaches(int generation) async {
try {
final prefs = await SharedPreferences.getInstance();
if (generation != _lookupCacheGeneration) return;
_restorePersistentCache(
prefs,
_metadataPersistentCacheKey,
_metadataCache,
);
_restorePersistentCache(
prefs,
_availabilityPersistentCacheKey,
_availabilityCache,
);
} catch (e) {
_log.w('Failed to load bridge lookup cache: $e');
}
}
static void _restorePersistentCache(
SharedPreferences prefs,
String prefsKey,
Map<String, _BridgeCacheEntry> target,
) {
final raw = prefs.getString(prefsKey);
if (raw == null || raw.isEmpty) return;
final decoded = jsonDecode(raw);
if (decoded is! Map) return;
final now = DateTime.now();
for (final entry in decoded.entries) {
if (target.length >= _bridgeCacheMaxEntries) break;
final key = entry.key.toString();
final rawEntry = entry.value;
if (key.isEmpty || rawEntry is! Map) continue;
final expiresAtMs = rawEntry['expires_at'];
final value = rawEntry['value'];
if (expiresAtMs is! int || value is! Map) continue;
final expiresAt = DateTime.fromMillisecondsSinceEpoch(expiresAtMs);
if (!expiresAt.isAfter(now)) continue;
target[key] = _BridgeCacheEntry(
value: _copyStringMap(Map<String, dynamic>.from(value)),
expiresAt: expiresAt,
);
}
}
static Future<void> _persistLookupCache(
Map<String, _BridgeCacheEntry> cache,
String prefsKey,
int generation,
) async {
try {
_pruneExpiredBridgeCache(cache);
final data = <String, dynamic>{
for (final entry in cache.entries)
entry.key: {
'expires_at': entry.value.expiresAt.millisecondsSinceEpoch,
'value': entry.value.value,
},
};
final prefs = await SharedPreferences.getInstance();
if (generation != _lookupCacheGeneration) return;
await prefs.setString(prefsKey, jsonEncode(data));
} catch (e) {
_log.w('Failed to persist bridge lookup cache: $e');
}
}
static Future<void> _clearPersistentLookupCaches() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_metadataPersistentCacheKey);
await prefs.remove(_availabilityPersistentCacheKey);
} catch (e) {
_log.w('Failed to clear bridge lookup cache: $e');
}
}
static Future<void> _clearLookupCaches() async {
_lookupCacheGeneration++;
_persistentLookupCacheLoadFuture = null;
_metadataCache.clear();
_availabilityCache.clear();
_metadataInFlight.clear();
_availabilityInFlight.clear();
for (final inFlight in _customSearchInFlight.values) {
_cancelExtensionRequestUnawaited(inFlight.requestId);
}
for (final inFlight in _homeFeedInFlight.values) {
_cancelExtensionRequestUnawaited(inFlight.requestId);
}
_customSearchInFlight.clear();
_homeFeedInFlight.clear();
await _clearPersistentLookupCaches();
}
static String _nextExtensionRequestId(String kind, String extensionId) {
_extensionRequestSequence++;
return [
kind,
DateTime.now().microsecondsSinceEpoch,
_extensionRequestSequence,
extensionId.trim(),
].join(':');
}
static void _cancelExtensionRequestUnawaited(String requestId) {
if (requestId.isEmpty) return;
unawaited(
cancelExtensionRequest(requestId).catchError((Object e) {
_log.w('Failed to cancel extension request $requestId: $e');
}),
);
}
static Future<void> cancelExtensionRequest(String requestId) async {
if (requestId.isEmpty) return;
await _channel.invokeMethod('cancelExtensionRequest', {
'request_id': requestId,
});
}
static void _cancelCustomSearchInFlightForScope(
String scopeKey, {
String? exceptKey,
}) {
for (final entry in _customSearchInFlight.entries.toList()) {
if (entry.key == exceptKey || entry.value.scopeKey != scopeKey) continue;
_cancelExtensionRequestUnawaited(entry.value.requestId);
}
}
static void cancelExtensionHomeFeedRequests() {
for (final inFlight in _homeFeedInFlight.values) {
_cancelExtensionRequestUnawaited(inFlight.requestId);
}
_homeFeedInFlight.clear();
}
static int _lookupCacheSize() {
_pruneExpiredBridgeCache(_metadataCache);
_pruneExpiredBridgeCache(_availabilityCache);
return _metadataCache.length + _availabilityCache.length;
}
static Future<Map<String, dynamic>> _invokeDownloadMethod(
String method,
DownloadRequestPayload payload,
) async {
final request = jsonEncode(payload.toJson());
final result = await _channel.invokeMethod(method, request);
return _decodeRequiredMapResult(result, method);
}
static Future<Map<String, dynamic>> downloadByStrategy({
required DownloadRequestPayload payload,
bool? useExtensions,
bool? useFallback,
}) async {
final routedPayload = payload.withStrategy(
useExtensions: useExtensions,
useFallback: useFallback,
);
_log.i(
'downloadByStrategy: "${payload.trackName}" by ${payload.artistName} '
'(service: ${payload.service}, ext: ${routedPayload.useExtensions}, fallback: ${routedPayload.useFallback})',
);
final response = await _invokeDownloadMethod(
'downloadByStrategy',
routedPayload,
);
if (response['success'] == true) {
final service = response['service'] ?? payload.service;
final filePath = response['file_path'] ?? '';
final bitDepth = response['actual_bit_depth'] as num?;
final sampleRate = response['actual_sample_rate'] as num?;
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;
}
static Future<Map<String, dynamic>> getDownloadProgress() async {
final result = await _channel.invokeMethod('getDownloadProgress');
return _decodeMapResult(result);
}
static Future<Map<String, dynamic>> getAllDownloadProgress() async {
final result = await _channel.invokeMethod('getAllDownloadProgress');
return _decodeMapResult(result);
}
static Stream<Map<String, dynamic>> downloadProgressStream() {
return _downloadProgressEvents.receiveBroadcastStream().map(
_decodeMapResult,
);
}
static Future<void> exitApp() async {
await _channel.invokeMethod('exitApp');
}
static Future<void> initItemProgress(String itemId) async {
await _channel.invokeMethod('initItemProgress', {'item_id': itemId});
}
static Future<void> finishItemProgress(String itemId) async {
await _channel.invokeMethod('finishItemProgress', {'item_id': itemId});
}
static Future<void> clearItemProgress(String itemId) async {
await _channel.invokeMethod('clearItemProgress', {'item_id': itemId});
}
static Future<void> cancelDownload(String itemId) async {
await _channel.invokeMethod('cancelDownload', {'item_id': itemId});
}
static Future<void> setDownloadDirectory(String path) async {
await _channel.invokeMethod('setDownloadDirectory', {'path': path});
}
static Future<void> setNetworkCompatibilityOptions({
required bool allowHttp,
required bool insecureTls,
}) async {
await _channel.invokeMethod('setNetworkCompatibilityOptions', {
'allow_http': allowHttp,
'insecure_tls': insecureTls,
});
}
static Future<Map<String, dynamic>> checkDuplicate(
String outputDir,
String isrc,
) async {
final result = await _channel.invokeMethod('checkDuplicate', {
'output_dir': outputDir,
'isrc': isrc,
});
return _decodeRequiredMapResult(result, 'checkDuplicate');
}
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;
}
static Future<String> sanitizeFilename(String filename) async {
final result = await _channel.invokeMethod('sanitizeFilename', {
'filename': filename,
});
return result as String;
}
static Future<Map<String, dynamic>?> pickSafTree() async {
final result = await _channel.invokeMethod('pickSafTree');
return _decodeNullableMapResult(result, 'pickSafTree');
}
static Future<bool> safExists(String uri) async {
final result = await _channel.invokeMethod('safExists', {'uri': uri});
return result as bool;
}
static Future<bool> safDelete(String uri) async {
final result = await _channel.invokeMethod('safDelete', {'uri': uri});
return result as bool;
}
static Future<Map<String, dynamic>> safStat(String uri) async {
final result = await _channel.invokeMethod('safStat', {'uri': uri});
return _decodeRequiredMapResult(result, 'safStat');
}
static Future<Map<String, dynamic>> resolveSafFile({
required String treeUri,
required String fileName,
String relativeDir = '',
}) async {
final result = await _channel.invokeMethod('resolveSafFile', {
'tree_uri': treeUri,
'relative_dir': relativeDir,
'file_name': fileName,
});
return _decodeRequiredMapResult(result, 'resolveSafFile');
}
static Future<String?> copyContentUriToTemp(String uri) async {
final result = await _channel.invokeMethod('safCopyToTemp', {'uri': uri});
return result as String?;
}
static Future<bool> replaceContentUriFromPath(
String uri,
String srcPath,
) async {
final result = await _channel.invokeMethod('safReplaceFromPath', {
'uri': uri,
'src_path': srcPath,
});
return result as bool;
}
static Future<String?> createSafFileFromPath({
required String treeUri,
required String relativeDir,
required String fileName,
required String mimeType,
required String srcPath,
}) async {
final result = await _channel.invokeMethod('safCreateFromPath', {
'tree_uri': treeUri,
'relative_dir': relativeDir,
'file_name': fileName,
'mime_type': mimeType,
'src_path': srcPath,
});
return result as String?;
}
static Future<void> openContentUri(String uri, {String mimeType = ''}) async {
await _channel.invokeMethod('openContentUri', {
'uri': uri,
'mime_type': mimeType,
});
}
static Future<bool> shareContentUri(String uri, {String title = ''}) async {
final result = await _channel.invokeMethod('shareContentUri', {
'uri': uri,
'title': title,
});
return result as bool? ?? false;
}
static Future<bool> shareMultipleContentUris(
List<String> uris, {
String title = '',
}) async {
final result = await _channel.invokeMethod('shareMultipleContentUris', {
'uris': uris,
'title': title,
});
return result as bool? ?? false;
}
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 _decodeRequiredMapResult(result, 'fetchLyrics');
}
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;
}
static Future<Map<String, dynamic>> getLyricsLRCWithSource(
String spotifyId,
String trackName,
String artistName, {
String? filePath,
int durationMs = 0,
}) async {
final result = await _channel.invokeMethod('getLyricsLRCWithSource', {
'spotify_id': spotifyId,
'track_name': trackName,
'artist_name': artistName,
'file_path': filePath ?? '',
'duration_ms': durationMs,
});
return _decodeRequiredMapResult(result, 'getLyricsLRCWithSource');
}
static Future<Map<String, dynamic>> embedLyricsToFile(
String filePath,
String lyrics,
) async {
final result = await _channel.invokeMethod('embedLyricsToFile', {
'file_path': filePath,
'lyrics': lyrics,
});
return _decodeRequiredMapResult(result, 'embedLyricsToFile');
}
static Future<void> cleanupConnections() async {
await _channel.invokeMethod('cleanupConnections');
}
static Future<Map<String, dynamic>> downloadCoverToFile(
String coverUrl,
String outputPath, {
bool maxQuality = true,
}) async {
final result = await _channel.invokeMethod('downloadCoverToFile', {
'cover_url': coverUrl,
'output_path': outputPath,
'max_quality': maxQuality,
});
return _decodeRequiredMapResult(result, 'downloadCoverToFile');
}
static Future<Map<String, dynamic>> extractCoverToFile(
String audioPath,
String outputPath,
) async {
final result = await _channel.invokeMethod('extractCoverToFile', {
'audio_path': audioPath,
'output_path': outputPath,
});
return _decodeRequiredMapResult(result, 'extractCoverToFile');
}
static Future<Map<String, dynamic>> fetchAndSaveLyrics({
required String trackName,
required String artistName,
required String spotifyId,
required int durationMs,
required String outputPath,
String audioFilePath = '',
}) async {
final result = await _channel.invokeMethod('fetchAndSaveLyrics', {
'track_name': trackName,
'artist_name': artistName,
'spotify_id': spotifyId,
'duration_ms': durationMs,
'output_path': outputPath,
'audio_file_path': audioFilePath,
});
return _decodeRequiredMapResult(result, 'fetchAndSaveLyrics');
}
/// Providers not in the list are disabled.
static Future<void> setLyricsProviders(List<String> providers) async {
final providersJSON = jsonEncode(providers);
await _channel.invokeMethod('setLyricsProviders', {
'providers_json': providersJSON,
});
}
static Future<List<String>> getLyricsProviders() async {
final result = await _channel.invokeMethod('getLyricsProviders');
return _decodeStringListResult(result, 'getLyricsProviders');
}
static Future<List<Map<String, dynamic>>>
getAvailableLyricsProviders() async {
final result = await _channel.invokeMethod('getAvailableLyricsProviders');
return _decodeMapListResult(result, 'getAvailableLyricsProviders');
}
/// Sets advanced lyrics fetch options used by provider-specific integrations.
static Future<void> setLyricsFetchOptions(
Map<String, dynamic> options,
) async {
final optionsJSON = jsonEncode(options);
await _channel.invokeMethod('setLyricsFetchOptions', {
'options_json': optionsJSON,
});
}
static Future<Map<String, dynamic>> getLyricsFetchOptions() async {
final result = await _channel.invokeMethod('getLyricsFetchOptions');
return _decodeRequiredMapResult(result, 'getLyricsFetchOptions');
}
static Future<Map<String, dynamic>> reEnrichFile(
Map<String, dynamic> request,
) async {
final requestJSON = jsonEncode(request);
final result = await _channel.invokeMethod('reEnrichFile', {
'request_json': requestJSON,
});
return _decodeRequiredMapResult(result, 'reEnrichFile');
}
static Future<Map<String, dynamic>> readFileMetadata(String filePath) async {
final result = await _channel.invokeMethod('readFileMetadata', {
'file_path': filePath,
});
return _decodeRequiredMapResult(result, 'readFileMetadata');
}
static Future<Map<String, dynamic>> editFileMetadata(
String filePath,
Map<String, String> metadata,
) async {
final metadataJSON = jsonEncode(metadata);
final result = await _channel.invokeMethod('editFileMetadata', {
'file_path': filePath,
'metadata_json': metadataJSON,
});
return _decodeRequiredMapResult(result, 'editFileMetadata');
}
/// Rewrites ARTIST/ALBUMARTIST Vorbis comments as multiple split entries
/// using the native Go FLAC writer, fixing FFmpeg's tag deduplication.
static Future<Map<String, dynamic>> rewriteSplitArtistTags(
String filePath,
String artist,
String albumArtist,
) async {
final result = await _channel.invokeMethod('rewriteSplitArtistTags', {
'file_path': filePath,
'artist': artist,
'album_artist': albumArtist,
});
return _decodeRequiredMapResult(result, 'rewriteSplitArtistTags');
}
static Future<bool> writeTempToSaf(String tempPath, String safUri) async {
final result = await _channel.invokeMethod('writeTempToSaf', {
'temp_path': tempPath,
'saf_uri': safUri,
});
final map = _decodeRequiredMapResult(result, 'writeTempToSaf');
return map['success'] == true;
}
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,
});
}
static Future<void> stopDownloadService() async {
await _channel.invokeMethod('stopDownloadService');
}
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,
});
}
static Future<bool> isDownloadServiceRunning() async {
final result = await _channel.invokeMethod('isDownloadServiceRunning');
return result as bool;
}
static Future<void> preWarmTrackCache(
List<Map<String, String>> tracks,
) async {
final tracksJson = jsonEncode(tracks);
await _channel.invokeMethod('preWarmTrackCache', {'tracks': tracksJson});
}
static Future<int> getTrackCacheSize() async {
await _ensurePersistentLookupCachesLoaded();
final result = await _channel.invokeMethod('getTrackCacheSize');
return (result as int) + _lookupCacheSize();
}
static Future<void> clearTrackCache() async {
await _clearLookupCaches();
await _channel.invokeMethod('clearTrackCache');
}
static Future<Map<String, dynamic>> searchProviderAll(
String providerId,
String query, {
int trackLimit = 15,
int artistLimit = 2,
String? filter,
}) async {
final result = await _channel.invokeMethod('searchProviderAll', {
'provider_id': providerId,
'query': query,
'track_limit': trackLimit,
'artist_limit': artistLimit,
'filter': filter ?? '',
});
return _decodeRequiredMapResult(result, 'searchProviderAll');
}
static Future<Map<String, dynamic>> getDeezerRelatedArtists(
String artistId, {
int limit = 12,
}) async {
final result = await _channel.invokeMethod('getDeezerRelatedArtists', {
'artist_id': artistId,
'limit': limit,
});
return _decodeRequiredMapResult(result, 'getDeezerRelatedArtists');
}
static Future<Map<String, dynamic>> parseProviderUrl(String url) async {
final result = await _channel.invokeMethod('parseProviderUrl', {
'url': url,
});
return _decodeRequiredMapResult(result, 'parseProviderUrl');
}
static Future<Map<String, dynamic>> getProviderMetadata(
String providerId,
String resourceType,
String resourceId,
) async {
final cacheKey = _providerMetadataCacheKey(
providerId,
resourceType,
resourceId,
);
await _ensurePersistentLookupCachesLoaded();
final cached = _getCachedMap(_metadataCache, cacheKey);
if (cached != null) return cached;
final inFlight = _metadataInFlight[cacheKey];
if (inFlight != null) return _copyStringMap(await inFlight);
final generation = _lookupCacheGeneration;
final future = _invokeCachedMap(
cacheKey,
_metadataCache,
() async {
final result = await _channel.invokeMethod('getProviderMetadata', {
'provider_id': providerId,
'resource_type': resourceType,
'resource_id': resourceId,
});
if (result == null) {
throw Exception(
'getProviderMetadata returned null for $providerId:$resourceType:$resourceId',
);
}
return _decodeRequiredMapResult(result, 'getProviderMetadata');
},
_metadataCacheTtl,
generation,
_metadataPersistentCacheKey,
);
_metadataInFlight[cacheKey] = future;
try {
return _copyStringMap(await future);
} finally {
_metadataInFlight.remove(cacheKey);
}
}
static Future<Map<String, dynamic>> searchDeezerByISRC(
String isrc, {
String? itemId,
}) async {
final result = await _channel.invokeMethod('searchDeezerByISRC', {
'isrc': isrc,
'item_id': itemId ?? '',
});
return _decodeRequiredMapResult(result, 'searchDeezerByISRC');
}
static Future<Map<String, String>?> getDeezerExtendedMetadata(
String trackId,
) async {
try {
final result = await _channel.invokeMethod('getDeezerExtendedMetadata', {
'track_id': trackId,
});
if (result == null) return null;
final data = _decodeRequiredMapResult(
result,
'getDeezerExtendedMetadata',
);
return {
'genre': data['genre'] as String? ?? '',
'label': data['label'] as String? ?? '',
'copyright': data['copyright'] as String? ?? '',
};
} catch (e) {
_log.w('Failed to get Deezer extended metadata for $trackId: $e');
return null;
}
}
static Future<Map<String, dynamic>> convertSpotifyToDeezer(
String resourceType,
String spotifyId,
) async {
final cacheKey = _providerMetadataCacheKey(
'spotify-to-deezer',
resourceType,
spotifyId,
);
await _ensurePersistentLookupCachesLoaded();
final cached = _getCachedMap(_metadataCache, cacheKey);
if (cached != null) return cached;
final inFlight = _metadataInFlight[cacheKey];
if (inFlight != null) return _copyStringMap(await inFlight);
final generation = _lookupCacheGeneration;
final future = _invokeCachedMap(
cacheKey,
_metadataCache,
() async {
final result = await _channel.invokeMethod('convertSpotifyToDeezer', {
'resource_type': resourceType,
'spotify_id': spotifyId,
});
return _decodeRequiredMapResult(result, 'convertSpotifyToDeezer');
},
_metadataCacheTtl,
generation,
_metadataPersistentCacheKey,
);
_metadataInFlight[cacheKey] = future;
try {
return _copyStringMap(await future);
} finally {
_metadataInFlight.remove(cacheKey);
}
}
static Future<List<Map<String, dynamic>>> getGoLogs() async {
final result = await _channel.invokeMethod('getLogs');
return _decodeMapListResult(result, 'getGoLogs');
}
static Future<Map<String, dynamic>> getGoLogsSince(int index) async {
final result = await _channel.invokeMethod('getLogsSince', {
'index': index,
});
return _decodeRequiredMapResult(result, 'getGoLogsSince');
}
static Future<void> clearGoLogs() async {
await _channel.invokeMethod('clearLogs');
}
static Future<int> getGoLogCount() async {
final result = await _channel.invokeMethod('getLogCount');
return result as int;
}
static Future<void> setGoLoggingEnabled(bool enabled) async {
await _channel.invokeMethod('setLoggingEnabled', {'enabled': enabled});
}
static Future<void> initExtensionSystem(
String extensionsDir,
String dataDir,
) async {
_log.d('initExtensionSystem: $extensionsDir, $dataDir');
await _channel.invokeMethod('initExtensionSystem', {
'extensions_dir': extensionsDir,
'data_dir': dataDir,
});
}
static Future<Map<String, dynamic>> loadExtensionsFromDir(
String dirPath,
) async {
_log.d('loadExtensionsFromDir: $dirPath');
final result = await _channel.invokeMethod('loadExtensionsFromDir', {
'dir_path': dirPath,
});
return _decodeRequiredMapResult(result, 'loadExtensionsFromDir');
}
static Future<Map<String, dynamic>> loadExtensionFromPath(
String filePath,
) async {
_log.d('loadExtensionFromPath: $filePath');
await _clearLookupCaches();
final result = await _channel.invokeMethod('loadExtensionFromPath', {
'file_path': filePath,
});
return _decodeRequiredMapResult(result, 'loadExtensionFromPath');
}
static Future<void> unloadExtension(String extensionId) async {
_log.d('unloadExtension: $extensionId');
await _clearLookupCaches();
await _channel.invokeMethod('unloadExtension', {
'extension_id': extensionId,
});
}
static Future<void> removeExtension(String extensionId) async {
_log.d('removeExtension: $extensionId');
await _clearLookupCaches();
await _channel.invokeMethod('removeExtension', {
'extension_id': extensionId,
});
}
static Future<Map<String, dynamic>> upgradeExtension(String filePath) async {
_log.d('upgradeExtension: $filePath');
await _clearLookupCaches();
final result = await _channel.invokeMethod('upgradeExtension', {
'file_path': filePath,
});
return _decodeRequiredMapResult(result, 'upgradeExtension');
}
static Future<Map<String, dynamic>> checkExtensionUpgrade(
String filePath,
) async {
_log.d('checkExtensionUpgrade: $filePath');
final result = await _channel.invokeMethod('checkExtensionUpgrade', {
'file_path': filePath,
});
return _decodeRequiredMapResult(result, 'checkExtensionUpgrade');
}
static Future<List<Map<String, dynamic>>> getInstalledExtensions() async {
final result = await _channel.invokeMethod('getInstalledExtensions');
return _decodeMapListResult(result, 'getInstalledExtensions');
}
static Future<void> setExtensionEnabled(
String extensionId,
bool enabled,
) async {
_log.d('setExtensionEnabled: $extensionId = $enabled');
await _clearLookupCaches();
await _channel.invokeMethod('setExtensionEnabled', {
'extension_id': extensionId,
'enabled': enabled,
});
}
static Future<void> setProviderPriority(List<String> providerIds) async {
_log.d('setProviderPriority: $providerIds');
await _clearLookupCaches();
await _channel.invokeMethod('setProviderPriority', {
'priority': jsonEncode(providerIds),
});
}
static Future<List<String>> getProviderPriority() async {
final result = await _channel.invokeMethod('getProviderPriority');
return _decodeStringListResult(result, 'getProviderPriority');
}
static Future<void> setDownloadFallbackExtensionIds(
List<String>? extensionIds,
) async {
_log.d('setDownloadFallbackExtensionIds: $extensionIds');
await _clearLookupCaches();
await _channel.invokeMethod('setDownloadFallbackExtensionIds', {
'extension_ids': extensionIds == null ? '' : jsonEncode(extensionIds),
});
}
static Future<void> setMetadataProviderPriority(
List<String> providerIds,
) async {
_log.d('setMetadataProviderPriority: $providerIds');
await _clearLookupCaches();
await _channel.invokeMethod('setMetadataProviderPriority', {
'priority': jsonEncode(providerIds),
});
}
static Future<List<String>> getMetadataProviderPriority() async {
final result = await _channel.invokeMethod('getMetadataProviderPriority');
return _decodeStringListResult(result, 'getMetadataProviderPriority');
}
static Future<Map<String, dynamic>> getExtensionSettings(
String extensionId,
) async {
final result = await _channel.invokeMethod('getExtensionSettings', {
'extension_id': extensionId,
});
return _decodeRequiredMapResult(result, 'getExtensionSettings');
}
static Future<void> setExtensionSettings(
String extensionId,
Map<String, dynamic> settings,
) async {
_log.d('setExtensionSettings: $extensionId');
await _clearLookupCaches();
await _channel.invokeMethod('setExtensionSettings', {
'extension_id': extensionId,
'settings': jsonEncode(settings),
});
}
static Future<Map<String, dynamic>> 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 _decodeRequiredMapResult(result, 'invokeExtensionAction');
}
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,
});
return _decodeMapListResult(result, 'searchTracksWithExtensions');
}
static Future<List<Map<String, dynamic>>> searchTracksWithMetadataProviders(
String query, {
int limit = 20,
bool includeExtensions = true,
}) async {
_log.d(
'searchTracksWithMetadataProviders: "$query", includeExtensions=$includeExtensions',
);
final result = await _channel.invokeMethod(
'searchTracksWithMetadataProviders',
{'query': query, 'limit': limit, 'include_extensions': includeExtensions},
);
return _decodeMapListResult(result, 'searchTracksWithMetadataProviders');
}
static Future<void> cleanupExtensions() async {
_log.d('cleanupExtensions');
await _channel.invokeMethod('cleanupExtensions');
}
static Future<Map<String, dynamic>?> getExtensionPendingAuth(
String extensionId,
) async {
final result = await _channel.invokeMethod('getExtensionPendingAuth', {
'extension_id': extensionId,
});
return _decodeNullableMapResult(result, 'getExtensionPendingAuth');
}
static Future<void> setExtensionAuthCode(
String extensionId,
String authCode,
) async {
_log.d('setExtensionAuthCode: $extensionId');
await _channel.invokeMethod('setExtensionAuthCode', {
'extension_id': extensionId,
'auth_code': authCode,
});
}
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,
});
}
static Future<void> clearExtensionPendingAuth(String extensionId) async {
await _channel.invokeMethod('clearExtensionPendingAuth', {
'extension_id': extensionId,
});
}
static Future<bool> isExtensionAuthenticated(String extensionId) async {
final result = await _channel.invokeMethod('isExtensionAuthenticated', {
'extension_id': extensionId,
});
return result as bool;
}
static Future<List<Map<String, dynamic>>> getAllPendingAuthRequests() async {
final result = await _channel.invokeMethod('getAllPendingAuthRequests');
return _decodeMapListResult(result, 'getAllPendingAuthRequests');
}
static Future<Map<String, dynamic>?> getPendingFFmpegCommand(
String commandId,
) async {
final result = await _channel.invokeMethod('getPendingFFmpegCommand', {
'command_id': commandId,
});
return _decodeNullableMapResult(result, 'getPendingFFmpegCommand');
}
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,
});
}
static Future<List<Map<String, dynamic>>>
getAllPendingFFmpegCommands() async {
final result = await _channel.invokeMethod('getAllPendingFFmpegCommands');
return _decodeMapListResult(result, 'setFFmpegCommandResult');
}
static Future<List<Map<String, dynamic>>> customSearchWithExtension(
String extensionId,
String query, {
Map<String, dynamic>? options,
bool cancelPrevious = false,
}) async {
final optionsJson = options != null ? jsonEncode(options) : '';
final scopeKey = 'customSearch:${extensionId.trim()}';
final cacheKey = [
scopeKey,
query,
jsonEncode(_canonicalizeJsonLike(options ?? const <String, dynamic>{})),
].join('\n');
final inFlight = _customSearchInFlight[cacheKey];
if (inFlight != null) return _copyMapList(await inFlight.future);
if (cancelPrevious) {
_cancelCustomSearchInFlightForScope(scopeKey, exceptKey: cacheKey);
}
final requestId = _nextExtensionRequestId('customSearch', extensionId);
final future = (() async {
final result = await _channel.invokeMethod('customSearchWithExtension', {
'extension_id': extensionId,
'query': query,
'options': optionsJson,
'request_id': requestId,
});
return _decodeMapListResult(result, 'customSearchWithExtension');
})();
final entry = _BridgeInFlight<List<Map<String, dynamic>>>(
requestId: requestId,
scopeKey: scopeKey,
future: future,
);
_customSearchInFlight[cacheKey] = entry;
try {
return _copyMapList(await future);
} finally {
if (identical(_customSearchInFlight[cacheKey], entry)) {
_customSearchInFlight.remove(cacheKey);
}
}
}
static Future<List<Map<String, dynamic>>> getSearchProviders() async {
final result = await _channel.invokeMethod('getSearchProviders');
return _decodeMapListResult(result, 'getSearchProviders');
}
static Future<List<Map<String, dynamic>>> getBuiltInProviders() async {
final result = await _channel.invokeMethod('getBuiltInProviders');
return _decodeMapListResult(result, 'getBuiltInProviders');
}
static Future<Map<String, dynamic>?> handleURLWithExtension(
String url,
) async {
try {
final result = await _channel.invokeMethod('handleURLWithExtension', {
'url': url,
});
return _decodeNullableMapResult(result, 'handleURLWithExtension');
} catch (e) {
return null;
}
}
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;
}
static Future<List<Map<String, dynamic>>> getURLHandlers() async {
final result = await _channel.invokeMethod('getURLHandlers');
return _decodeMapListResult(result, 'getURLHandlers');
}
static Future<Map<String, dynamic>?> getExtensionHomeFeed(
String extensionId, {
bool cancelPrevious = false,
}) async {
final cacheKey = 'homeFeed:${extensionId.trim()}';
final inFlight = _homeFeedInFlight[cacheKey];
if (inFlight != null) {
if (!cancelPrevious) {
return _copyNullableStringMap(await inFlight.future);
}
_cancelExtensionRequestUnawaited(inFlight.requestId);
_homeFeedInFlight.remove(cacheKey);
}
final requestId = _nextExtensionRequestId('homeFeed', extensionId);
final future = (() async {
try {
final result = await _channel.invokeMethod('getExtensionHomeFeed', {
'extension_id': extensionId,
'request_id': requestId,
});
return _decodeNullableMapResult(result, 'getExtensionHomeFeed');
} catch (e) {
_log.e('getExtensionHomeFeed failed: $e');
return null;
}
})();
final entry = _BridgeInFlight<Map<String, dynamic>?>(
requestId: requestId,
scopeKey: cacheKey,
future: future,
);
_homeFeedInFlight[cacheKey] = entry;
try {
return _copyNullableStringMap(await future);
} finally {
if (identical(_homeFeedInFlight[cacheKey], entry)) {
_homeFeedInFlight.remove(cacheKey);
}
}
}
static Future<Map<String, dynamic>?> getExtensionBrowseCategories(
String extensionId,
) async {
try {
final result = await _channel.invokeMethod(
'getExtensionBrowseCategories',
{'extension_id': extensionId},
);
return _decodeNullableMapResult(result, 'getExtensionBrowseCategories');
} catch (e) {
_log.e('getExtensionBrowseCategories failed: $e');
return null;
}
}
static Future<void> setLibraryCoverCacheDir(String cacheDir) async {
_log.i('setLibraryCoverCacheDir: $cacheDir');
await _channel.invokeMethod('setLibraryCoverCacheDir', {
'cache_dir': cacheDir,
});
}
static Future<List<Map<String, dynamic>>> scanLibraryFolder(
String folderPath,
) async {
_log.i('scanLibraryFolder: $folderPath');
final result = await _channel.invokeMethod('scanLibraryFolder', {
'folder_path': folderPath,
});
return _decodeMapListResultAsync(result, 'scanLibraryFolder');
}
static Future<Map<String, dynamic>> scanLibraryFolderIncremental(
String folderPath,
Map<String, int> existingFiles,
) async {
_log.i(
'scanLibraryFolderIncremental: $folderPath (${existingFiles.length} existing files)',
);
final result = await _channel.invokeMethod('scanLibraryFolderIncremental', {
'folder_path': folderPath,
'existing_files': jsonEncode(existingFiles),
});
return _decodeRequiredMapResultAsync(
result,
'scanLibraryFolderIncremental',
);
}
static Future<Map<String, dynamic>> scanLibraryFolderIncrementalFromSnapshot(
String folderPath,
String snapshotPath,
) async {
final result = await _channel.invokeMethod(
'scanLibraryFolderIncrementalFromSnapshot',
{'folder_path': folderPath, 'snapshot_path': snapshotPath},
);
return _decodeRequiredMapResultAsync(
result,
'scanLibraryFolderIncrementalFromSnapshot',
);
}
static Future<List<Map<String, dynamic>>> scanSafTree(String treeUri) async {
_log.i('scanSafTree: $treeUri');
final result = await _channel.invokeMethod('scanSafTree', {
'tree_uri': treeUri,
});
return _decodeMapListResultAsync(result, 'scanSafTree');
}
static Future<Map<String, dynamic>> scanSafTreeIncremental(
String treeUri,
Map<String, int> existingFiles,
) async {
_log.i(
'scanSafTreeIncremental: $treeUri (${existingFiles.length} existing files)',
);
final result = await _channel.invokeMethod('scanSafTreeIncremental', {
'tree_uri': treeUri,
'existing_files': jsonEncode(existingFiles),
});
return _decodeRequiredMapResultAsync(result, 'scanSafTreeIncremental');
}
static Future<Map<String, dynamic>> scanSafTreeIncrementalFromSnapshot(
String treeUri,
String snapshotPath,
) async {
final result = await _channel.invokeMethod(
'scanSafTreeIncrementalFromSnapshot',
{'tree_uri': treeUri, 'snapshot_path': snapshotPath},
);
return _decodeRequiredMapResultAsync(
result,
'scanSafTreeIncrementalFromSnapshot',
);
}
static Future<Map<String, int>> getSafFileModTimes(List<String> uris) async {
final result = await _channel.invokeMethod('getSafFileModTimes', {
'uris': jsonEncode(uris),
});
final map = _decodeRequiredMapResult(result, 'getSafFileModTimes');
return map.map((key, value) => MapEntry(key, (value as num).toInt()));
}
static Future<Map<String, dynamic>> getLibraryScanProgress() async {
final result = await _channel.invokeMethod('getLibraryScanProgress');
return _decodeMapResult(result);
}
static Stream<Map<String, dynamic>> libraryScanProgressStream() {
return _libraryScanProgressEvents.receiveBroadcastStream().map(
_decodeMapResult,
);
}
static Future<void> cancelLibraryScan() async {
await _channel.invokeMethod('cancelLibraryScan');
}
static Object? _decodeJsonResult(dynamic result) {
if (result is String) {
if (result.isEmpty) return null;
return jsonDecode(result);
}
return result;
}
static Future<Object?> _decodeJsonResultAsync(dynamic result) async {
if (result is Map && result[_jsonResultFileKey] is String) {
final file = File(result[_jsonResultFileKey] as String);
try {
final contents = await file.readAsString();
if (contents.isEmpty) return null;
return jsonDecode(contents);
} finally {
try {
await file.delete();
} catch (_) {}
}
}
return _decodeJsonResult(result);
}
static Map<String, dynamic> _decodeRequiredMapResult(
dynamic result,
String method,
) {
final decoded = _decodeJsonResult(result);
if (decoded is Map) {
return decoded.cast<String, dynamic>();
}
throw FormatException(
'Expected map result from $method, got ${decoded.runtimeType}',
);
}
static Map<String, dynamic>? _decodeNullableMapResult(
dynamic result,
String method,
) {
final decoded = _decodeJsonResult(result);
if (decoded == null) return null;
if (decoded is Map) {
return decoded.cast<String, dynamic>();
}
throw FormatException(
'Expected nullable map result from $method, got ${decoded.runtimeType}',
);
}
static Future<Map<String, dynamic>> _decodeRequiredMapResultAsync(
dynamic result,
String method,
) async {
final decoded = await _decodeJsonResultAsync(result);
if (decoded is Map) {
return decoded.cast<String, dynamic>();
}
throw FormatException(
'Expected map result from $method, got ${decoded.runtimeType}',
);
}
static List<dynamic> _decodeRequiredListResult(
dynamic result,
String method,
) {
final decoded = _decodeJsonResult(result);
if (decoded is List) return decoded;
throw FormatException(
'Expected list result from $method, got ${decoded.runtimeType}',
);
}
static Future<List<dynamic>> _decodeRequiredListResultAsync(
dynamic result,
String method,
) async {
final decoded = await _decodeJsonResultAsync(result);
if (decoded is List) return decoded;
throw FormatException(
'Expected list result from $method, got ${decoded.runtimeType}',
);
}
static List<Map<String, dynamic>> _decodeMapListResult(
dynamic result,
String method,
) {
return _decodeRequiredListResult(result, method).map((entry) {
if (entry is Map) return entry.cast<String, dynamic>();
throw FormatException(
'Expected map entry from $method, got ${entry.runtimeType}',
);
}).toList();
}
static Future<List<Map<String, dynamic>>> _decodeMapListResultAsync(
dynamic result,
String method,
) async {
final decoded = await _decodeRequiredListResultAsync(result, method);
return decoded.map((entry) {
if (entry is Map) return entry.cast<String, dynamic>();
throw FormatException(
'Expected map entry from $method, got ${entry.runtimeType}',
);
}).toList();
}
static List<String> _decodeStringListResult(dynamic result, String method) {
return _decodeRequiredListResult(result, method).map((entry) {
if (entry is String) return entry;
throw FormatException(
'Expected string entry from $method, got ${entry.runtimeType}',
);
}).toList();
}
static Map<String, dynamic> _decodeMapResult(dynamic result) {
if (result is Map) {
return result.cast<String, dynamic>();
}
if (result is String) {
if (result.isEmpty) return const <String, dynamic>{};
final decoded = jsonDecode(result);
if (decoded is Map) {
return decoded.cast<String, dynamic>();
}
}
return const <String, dynamic>{};
}
// MARK: - iOS Security-Scoped Bookmark
/// Create a security-scoped bookmark from a filesystem path picked by
/// FilePicker on iOS. Must be called while the picker session is still active.
/// Returns base64-encoded bookmark data, or null on failure.
static Future<String?> createIosBookmarkFromPath(String path) async {
try {
final result = await _channel.invokeMethod('createIosBookmarkFromPath', {
'path': path,
});
return result as String?;
} catch (e) {
_log.w('Failed to create iOS bookmark from path: $e');
return null;
}
}
/// Resolve a base64-encoded iOS security-scoped bookmark and start accessing
/// the resource. Returns the resolved filesystem path.
/// The resource stays accessed until [stopAccessingIosBookmark] is called.
static Future<String?> startAccessingIosBookmark(String bookmark) async {
try {
final result = await _channel.invokeMethod('startAccessingIosBookmark', {
'bookmark': bookmark,
});
return result as String?;
} catch (e) {
_log.w('Failed to start accessing iOS bookmark: $e');
return null;
}
}
/// Stop accessing the currently active iOS security-scoped resource.
static Future<void> stopAccessingIosBookmark() async {
try {
await _channel.invokeMethod('stopAccessingIosBookmark');
} catch (e) {
_log.w('Failed to stop accessing iOS bookmark: $e');
}
}
static Future<Map<String, dynamic>?> readAudioMetadata(
String filePath,
) async {
try {
final result = await _channel.invokeMethod('readAudioMetadata', {
'file_path': filePath,
});
return _decodeNullableMapResult(result, 'readAudioMetadata');
} catch (e) {
_log.w('Failed to read audio metadata: $e');
return null;
}
}
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 _decodeRequiredMapResult(result, 'runPostProcessing');
}
static Future<Map<String, dynamic>> runPostProcessingV2(
String filePath, {
Map<String, dynamic>? metadata,
}) async {
final input = <String, dynamic>{};
if (filePath.startsWith('content://')) {
input['uri'] = filePath;
} else {
input['path'] = filePath;
}
final result = await _channel.invokeMethod('runPostProcessingV2', {
'input': jsonEncode(input),
'metadata': metadata != null ? jsonEncode(metadata) : '',
});
return _decodeRequiredMapResult(result, 'runPostProcessingV2');
}
static Future<List<Map<String, dynamic>>> getPostProcessingProviders() async {
final result = await _channel.invokeMethod('getPostProcessingProviders');
return _decodeMapListResult(result, 'getPostProcessingProviders');
}
static Future<void> initExtensionStore(String cacheDir) async {
_log.d('initExtensionStore: $cacheDir');
await _channel.invokeMethod('initExtensionStore', {'cache_dir': cacheDir});
}
static Future<void> setStoreRegistryUrl(String registryUrl) async {
_log.d('setStoreRegistryUrl: $registryUrl');
await _channel.invokeMethod('setStoreRegistryUrl', {
'registry_url': registryUrl,
});
}
static Future<String> getStoreRegistryUrl() async {
_log.d('getStoreRegistryUrl');
final result = await _channel.invokeMethod('getStoreRegistryUrl');
return result as String? ?? '';
}
static Future<void> clearStoreRegistryUrl() async {
_log.d('clearStoreRegistryUrl');
await _channel.invokeMethod('clearStoreRegistryUrl');
}
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,
});
return _decodeMapListResult(result, 'getStoreExtensions');
}
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 ?? '',
});
return _decodeMapListResult(result, 'searchStoreExtensions');
}
static Future<List<String>> getStoreCategories() async {
final result = await _channel.invokeMethod('getStoreCategories');
return _decodeStringListResult(result, 'getStoreCategories');
}
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;
}
static Future<void> clearStoreCache() async {
_log.d('clearStoreCache');
await _channel.invokeMethod('clearStoreCache');
}
static Future<Map<String, dynamic>> parseCueSheet(
String cuePath, {
String audioDir = '',
}) async {
_log.i('parseCueSheet: $cuePath (audioDir: $audioDir)');
final result = await _channel.invokeMethod('parseCueSheet', {
'cue_path': cuePath,
'audio_dir': audioDir,
});
return _decodeRequiredMapResult(result, 'parseCueSheet');
}
}