fix: native FLAC handling and extension API optimizations

Native FLAC handling:
- Properly detect and publish native FLAC payloads inside MP4 containers
- Rename to .flac extension and embed metadata instead of skipping
- Fix all code paths: SAF, non-SAF, and native worker finalizer

Extension API optimizations:
- Enable response compression for API/search calls (faster metadata loads)
- Keep downloads uncompressed for accurate progress/streaming
- Add separate extensionAPITransport with compression enabled

Platform bridge caching:
- Cache handleURLWithExtension results (5 min TTL)
- Cache customSearchWithExtension results (2 min TTL)
- Prevent duplicate in-flight requests for same URL/query

Dependency cleanup:
- Remove unused sqflite_common_ffi and sqlite3 packages
This commit is contained in:
zarzet
2026-05-15 00:54:58 +07:00
parent 629eb66595
commit 012dcdc2dd
7 changed files with 314 additions and 46 deletions
+138 -6
View File
@@ -21,6 +21,15 @@ class _BridgeCacheEntry {
bool get isExpired => DateTime.now().isAfter(expiresAt);
}
class _BridgeListCacheEntry {
final List<Map<String, dynamic>> value;
final DateTime expiresAt;
const _BridgeListCacheEntry({required this.value, required this.expiresAt});
bool get isExpired => DateTime.now().isAfter(expiresAt);
}
class _BridgeInFlight<T> {
final String requestId;
final String scopeKey;
@@ -39,6 +48,8 @@ class PlatformBridge {
static const _backgroundJsonDecodeThresholdBytes = 128 * 1024;
static const _metadataCacheTtl = Duration(minutes: 20);
static const _availabilityCacheTtl = Duration(minutes: 15);
static const _urlHandleCacheTtl = Duration(minutes: 5);
static const _customSearchCacheTtl = Duration(minutes: 2);
static const _bridgeCacheMaxEntries = 256;
static const _metadataPersistentCacheKey = 'bridge_metadata_lookup_cache_v1';
static const _availabilityPersistentCacheKey =
@@ -51,9 +62,13 @@ class PlatformBridge {
);
static final Map<String, _BridgeCacheEntry> _metadataCache = {};
static final Map<String, _BridgeCacheEntry> _availabilityCache = {};
static final Map<String, _BridgeCacheEntry> _urlHandleCache = {};
static final Map<String, _BridgeListCacheEntry> _customSearchCache = {};
static final Map<String, Future<Map<String, dynamic>>> _metadataInFlight = {};
static final Map<String, Future<Map<String, dynamic>>> _availabilityInFlight =
{};
static final Map<String, Future<Map<String, dynamic>?>> _urlHandleInFlight =
{};
static final Map<String, _BridgeInFlight<List<Map<String, dynamic>>>>
_customSearchInFlight = {};
static final Map<String, _BridgeInFlight<Map<String, dynamic>?>>
@@ -329,8 +344,11 @@ class PlatformBridge {
_persistentLookupCacheLoadFuture = null;
_metadataCache.clear();
_availabilityCache.clear();
_urlHandleCache.clear();
_customSearchCache.clear();
_metadataInFlight.clear();
_availabilityInFlight.clear();
_urlHandleInFlight.clear();
for (final inFlight in _customSearchInFlight.values) {
_cancelExtensionRequestUnawaited(inFlight.requestId);
}
@@ -1388,7 +1406,11 @@ class PlatformBridge {
_cancelCustomSearchInFlightForScope(scopeKey, exceptKey: cacheKey);
}
final cached = _getCachedMapList(_customSearchCache, cacheKey);
if (cached != null) return cached;
final requestId = _nextExtensionRequestId('customSearch', extensionId);
final generation = _lookupCacheGeneration;
final future = (() async {
final result = await _channel.invokeMethod('customSearchWithExtension', {
'extension_id': extensionId,
@@ -1396,7 +1418,16 @@ class PlatformBridge {
'options': optionsJson,
'request_id': requestId,
});
return _decodeMapListResult(result, 'customSearchWithExtension');
final decoded = _decodeMapListResult(result, 'customSearchWithExtension');
if (generation == _lookupCacheGeneration) {
_putMemoryCachedMapList(
_customSearchCache,
cacheKey,
decoded,
_customSearchCacheTtl,
);
}
return decoded;
})();
final entry = _BridgeInFlight<List<Map<String, dynamic>>>(
@@ -1422,14 +1453,115 @@ class PlatformBridge {
static Future<Map<String, dynamic>?> handleURLWithExtension(
String url,
) async {
final cacheKey = url.trim();
if (cacheKey.isEmpty) return null;
final cached = _getCachedMap(_urlHandleCache, cacheKey);
if (cached != null) return cached;
final inFlight = _urlHandleInFlight[cacheKey];
if (inFlight != null) return _copyNullableStringMap(await inFlight);
final generation = _lookupCacheGeneration;
final future = (() async {
try {
final result = await _channel.invokeMethod('handleURLWithExtension', {
'url': url,
});
final decoded = _decodeNullableMapResult(
result,
'handleURLWithExtension',
);
if (generation == _lookupCacheGeneration &&
decoded != null &&
_isCacheableURLHandleResult(decoded)) {
_putMemoryCachedMap(
_urlHandleCache,
cacheKey,
decoded,
_urlHandleCacheTtl,
);
}
return decoded;
} catch (e) {
return null;
}
})();
_urlHandleInFlight[cacheKey] = future;
try {
final result = await _channel.invokeMethod('handleURLWithExtension', {
'url': url,
});
return _decodeNullableMapResult(result, 'handleURLWithExtension');
} catch (e) {
return _copyNullableStringMap(await future);
} finally {
if (identical(_urlHandleInFlight[cacheKey], future)) {
_urlHandleInFlight.remove(cacheKey);
}
}
}
static bool _isCacheableURLHandleResult(Map<String, dynamic> result) {
final type = result['type']?.toString();
if (type == null || type.isEmpty) return false;
if (type == 'track') {
final track = result['track'];
if (track is! Map) return false;
final name = track['name']?.toString().trim() ?? '';
return name.isNotEmpty;
}
return type == 'album' || type == 'playlist' || type == 'artist';
}
static void _putMemoryCachedMap(
Map<String, _BridgeCacheEntry> cache,
String key,
Map<String, dynamic> value,
Duration ttl,
) {
_pruneExpiredBridgeCache(cache);
while (cache.length >= _bridgeCacheMaxEntries && cache.isNotEmpty) {
cache.remove(cache.keys.first);
}
cache[key] = _BridgeCacheEntry(
value: _copyStringMap(value),
expiresAt: DateTime.now().add(ttl),
);
}
static List<Map<String, dynamic>>? _getCachedMapList(
Map<String, _BridgeListCacheEntry> cache,
String key,
) {
_pruneExpiredBridgeListCache(cache);
final entry = cache[key];
if (entry == null) return null;
if (entry.isExpired) {
cache.remove(key);
return null;
}
return _copyMapList(entry.value);
}
static void _putMemoryCachedMapList(
Map<String, _BridgeListCacheEntry> cache,
String key,
List<Map<String, dynamic>> value,
Duration ttl,
) {
_pruneExpiredBridgeListCache(cache);
while (cache.length >= _bridgeCacheMaxEntries && cache.isNotEmpty) {
cache.remove(cache.keys.first);
}
cache[key] = _BridgeListCacheEntry(
value: _copyMapList(value),
expiresAt: DateTime.now().add(ttl),
);
}
static void _pruneExpiredBridgeListCache(
Map<String, _BridgeListCacheEntry> cache,
) {
if (cache.isEmpty) return;
final now = DateTime.now();
cache.removeWhere((_, entry) => now.isAfter(entry.expiresAt));
}
static Future<String?> findURLHandler(String url) async {