diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 291e8121..c707c801 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -1868,18 +1868,11 @@ class MainActivity: FlutterFragmentActivity() { val query = call.argument("query") ?: "" val limit = call.argument("limit") ?: 20 val response = withContext(Dispatchers.IO) { - val method = Gobackend::class.java.getMethod( - "searchTracksWithMetadataProviderJSON", - String::class.java, - String::class.java, - java.lang.Long.TYPE - ) - method.invoke( - null, + Gobackend.searchTracksWithMetadataProviderJSON( extensionId, query, limit.toLong() - ) as? String ?: "[]" + ) } result.success(response) } diff --git a/lib/providers/extension_models.dart b/lib/providers/extension_models.dart index 2adff7b3..55d54efd 100644 --- a/lib/providers/extension_models.dart +++ b/lib/providers/extension_models.dart @@ -361,6 +361,20 @@ class SearchFilter { } } +String canonicalExtensionSearchFilterId(String value) { + final normalized = value.trim().toLowerCase().replaceAll( + RegExp(r'[^a-z0-9]+'), + '', + ); + return switch (normalized) { + 'track' || 'tracks' || 'song' || 'songs' || 'music' => 'track', + 'artist' || 'artists' => 'artist', + 'album' || 'albums' => 'album', + 'playlist' || 'playlists' => 'playlist', + _ => normalized, + }; +} + class SearchBehavior { final bool enabled; final String? placeholder; @@ -399,6 +413,23 @@ class SearchBehavior { ); } + String? filterIdForKind(String kind) { + final canonicalKind = canonicalExtensionSearchFilterId(kind); + for (final filter in filters) { + if (canonicalExtensionSearchFilterId(filter.id) == canonicalKind || + (filter.label != null && + canonicalExtensionSearchFilterId(filter.label!) == + canonicalKind) || + (filter.icon != null && + canonicalExtensionSearchFilterId(filter.icon!) == + canonicalKind)) { + final id = filter.id.trim(); + if (id.isNotEmpty) return id; + } + } + return null; + } + (double, double) getThumbnailSize({double defaultSize = 56}) { if (thumbnailWidth != null && thumbnailHeight != null) { return (thumbnailWidth!.toDouble(), thumbnailHeight!.toDouble()); diff --git a/lib/screens/home_search_logic.dart b/lib/screens/home_search_logic.dart index 8d7fe487..6deb4aab 100644 --- a/lib/screens/home_search_logic.dart +++ b/lib/screens/home_search_logic.dart @@ -183,32 +183,11 @@ class HomeSearchProviderPolicy { return null; } - return filters - .where( - (candidate) => - canonicalFilterId(candidate.id) == canonicalFilter || - (candidate.label != null && - canonicalFilterId(candidate.label!) == canonicalFilter) || - (candidate.icon != null && - canonicalFilterId(candidate.icon!) == canonicalFilter), - ) - .firstOrNull - ?.id; + return extension?.searchBehavior?.filterIdForKind(canonicalFilter); } - static String canonicalFilterId(String value) { - final normalized = value.trim().toLowerCase().replaceAll( - RegExp(r'[^a-z0-9]+'), - '', - ); - return switch (normalized) { - 'track' || 'tracks' || 'song' || 'songs' || 'music' => 'track', - 'artist' || 'artists' => 'artist', - 'album' || 'albums' => 'album', - 'playlist' || 'playlists' => 'playlist', - _ => normalized, - }; - } + static String canonicalFilterId(String value) => + canonicalExtensionSearchFilterId(value); static String? preferredFilter( String preferredSearchTab, diff --git a/lib/screens/track_metadata_edit_sheet.dart b/lib/screens/track_metadata_edit_sheet.dart index 72d21fc1..2fa4172c 100644 --- a/lib/screens/track_metadata_edit_sheet.dart +++ b/lib/screens/track_metadata_edit_sheet.dart @@ -870,16 +870,17 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { context, listen: false, ).read(extensionProvider); - final selectedProviderId = - configuredProviderId != null && - extensionState.extensions.any( - (extension) => - extension.id == configuredProviderId && - extension.enabled && - extension.hasMetadataProvider, - ) - ? configuredProviderId - : null; + final selectedProvider = configuredProviderId == null + ? null + : extensionState.extensions + .where( + (extension) => + extension.id == configuredProviderId && + extension.enabled && + extension.hasMetadataProvider, + ) + .firstOrNull; + final selectedProviderId = selectedProvider?.id; final usesAutomaticProvider = selectedProviderId == null || selectedProviderId.isEmpty; final shouldFetchLyrics = _autoFillFields.contains('lyrics'); @@ -920,16 +921,31 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { if (needsTrackLookup && best == null) { final query = queryParts.join(' '); - final results = usesAutomaticProvider - ? await PlatformBridge.searchTracksWithMetadataProviders( - query, - limit: 5, - ) - : await PlatformBridge.searchTracksWithMetadataProvider( - selectedProviderId, - query, - limit: 5, - ); + final List> results; + if (usesAutomaticProvider) { + results = await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 5, + ); + } else if (selectedProvider!.hasCustomSearch) { + final trackFilter = selectedProvider.searchBehavior?.filterIdForKind( + 'track', + ); + results = await PlatformBridge.customSearchWithExtension( + selectedProvider.id, + query, + options: { + 'limit': 5, + if (trackFilter != null) 'filter': trackFilter, + }, + ); + } else { + results = await PlatformBridge.searchTracksWithMetadataProvider( + selectedProvider!.id, + query, + limit: 5, + ); + } if (!mounted) return; @@ -1186,7 +1202,8 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { coverUrl: coverUrl?.isNotEmpty == true ? coverUrl : null, ); }); - } catch (e) { + } catch (e, stackTrace) { + _log.e('Metadata auto-fill failed: $e', e, stackTrace); if (mounted) { _showSheetSnackBar( context.l10n.snackbarError(context.friendlyError(e)), diff --git a/test/maintainability_contracts_test.dart b/test/maintainability_contracts_test.dart index ef4eb750..f353d74e 100644 --- a/test/maintainability_contracts_test.dart +++ b/test/maintainability_contracts_test.dart @@ -173,6 +173,8 @@ void main() { ]), 'songs', ); + expect(extension.searchBehavior?.filterIdForKind('track'), 'songs'); + expect(extension.searchBehavior?.filterIdForKind('album'), 'records'); expect( HomeSearchProviderPolicy.displayFilterSelection( null, diff --git a/test/platform_bridge_metadata_test.dart b/test/platform_bridge_metadata_test.dart index 7dba5284..9861143b 100644 --- a/test/platform_bridge_metadata_test.dart +++ b/test/platform_bridge_metadata_test.dart @@ -104,4 +104,30 @@ void main() { }); expect(results.single['provider_id'], 'selected-metadata'); }); + + test('metadata custom search sends the manifest track filter', () async { + MethodCall? capturedCall; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(backendChannel, (call) async { + capturedCall = call; + return jsonEncode([ + {'id': 'track-1', 'name': 'Song', 'provider_id': 'custom-metadata'}, + ]); + }); + + final results = await PlatformBridge.customSearchWithExtension( + 'custom-metadata', + 'Song Artist', + options: {'limit': 5, 'filter': 'songs'}, + ); + + expect(capturedCall?.method, 'customSearchWithExtension'); + final arguments = capturedCall?.arguments as Map?; + expect(arguments, isNotNull); + expect(arguments?['extension_id'], 'custom-metadata'); + expect(arguments?['query'], 'Song Artist'); + expect(arguments?['options'], jsonEncode({'limit': 5, 'filter': 'songs'})); + expect(arguments?['request_id'], isA()); + expect(results.single['provider_id'], 'custom-metadata'); + }); }