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 43460b4e..36d81b93 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -2734,16 +2734,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(null) } - "searchDeezerAll" -> { - val query = call.argument("query") ?: "" - val trackLimit = call.argument("track_limit") ?: 15 - val artistLimit = call.argument("artist_limit") ?: 2 - val filter = call.argument("filter") ?: "" - val response = withContext(Dispatchers.IO) { - Gobackend.searchDeezerAll(query, trackLimit.toLong(), artistLimit.toLong(), filter) - } - result.success(response) - } "searchTidalAll" -> { val query = call.argument("query") ?: "" val trackLimit = call.argument("track_limit") ?: 15 diff --git a/go_backend/exports.go b/go_backend/exports.go index 297b5b36..eac4b4f4 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1842,24 +1842,6 @@ func ClearTrackIDCache() { ClearTrackCache() } -func SearchDeezerAll(query string, trackLimit, artistLimit int, filter string) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - - client := GetDeezerClient() - results, err := client.SearchAll(ctx, query, trackLimit, artistLimit, filter) - if err != nil { - return "", err - } - - jsonBytes, err := json.Marshal(results) - if err != nil { - return "", err - } - - return string(jsonBytes), nil -} - func SearchTidalAll(query string, trackLimit, artistLimit int, filter string) (string, error) { downloader := NewTidalDownloader() results, err := downloader.SearchAll(query, trackLimit, artistLimit, filter) diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index aba4f3d5..60c609d7 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -1,7 +1,6 @@ package gobackend import ( - "context" "encoding/json" "errors" "fmt" @@ -895,7 +894,7 @@ func SetMetadataProviderPriority(providerIDs []string) { metadataProviderPriorityMu.Lock() defer metadataProviderPriorityMu.Unlock() - sanitized := make([]string, 0, len(providerIDs)+3) + sanitized := make([]string, 0, len(providerIDs)+2) seen := map[string]struct{}{} for _, providerID := range providerIDs { providerID = strings.TrimSpace(providerID) @@ -908,7 +907,7 @@ func SetMetadataProviderPriority(providerIDs []string) { seen[providerID] = struct{}{} sanitized = append(sanitized, providerID) } - for _, providerID := range []string{"deezer", "qobuz", "tidal"} { + for _, providerID := range []string{"qobuz", "tidal"} { if _, exists := seen[providerID]; exists { continue } @@ -925,7 +924,7 @@ func GetMetadataProviderPriority() []string { defer metadataProviderPriorityMu.RUnlock() if len(metadataProviderPriority) == 0 { - return []string{"deezer", "qobuz", "tidal"} + return []string{"qobuz", "tidal"} } result := make([]string, len(metadataProviderPriority)) @@ -935,7 +934,7 @@ func GetMetadataProviderPriority() []string { func isBuiltInProvider(providerID string) bool { switch providerID { - case "tidal", "qobuz", "deezer": + case "tidal", "qobuz": return true default: return false @@ -1006,20 +1005,6 @@ func metadataTrackDedupKey(track ExtTrackMetadata) string { func searchBuiltInMetadataTracks(providerID, query string, limit int) ([]ExtTrackMetadata, error) { switch providerID { - case "deezer": - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - - results, err := GetDeezerClient().SearchAll(ctx, query, limit, 0, "track") - if err != nil { - return nil, err - } - - tracks := make([]ExtTrackMetadata, 0, len(results.Tracks)) - for _, track := range results.Tracks { - tracks = append(tracks, normalizeBuiltInMetadataTrack(track, "deezer")) - } - return tracks, nil case "qobuz": return NewQobuzDownloader().SearchTracks(query, limit) case "tidal": diff --git a/go_backend/extension_providers_test.go b/go_backend/extension_providers_test.go index 2a3eac78..79616095 100644 --- a/go_backend/extension_providers_test.go +++ b/go_backend/extension_providers_test.go @@ -12,7 +12,7 @@ func TestSetMetadataProviderPriorityAddsBuiltIns(t *testing.T) { SetMetadataProviderPriority([]string{"tidal"}) got := GetMetadataProviderPriority() - want := []string{"tidal", "deezer", "qobuz"} + want := []string{"tidal", "qobuz"} if len(got) != len(want) { t.Fatalf("unexpected priority length: got %v want %v", got, want) } @@ -208,7 +208,7 @@ func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) { searchBuiltInMetadataTracksFunc = originalSearch }() - SetMetadataProviderPriority([]string{"qobuz", "tidal", "deezer"}) + SetMetadataProviderPriority([]string{"qobuz", "tidal"}) var calls []string searchBuiltInMetadataTracksFunc = func(providerID, query string, limit int) ([]ExtTrackMetadata, error) { @@ -223,10 +223,6 @@ func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) { {ProviderID: "tidal", SpotifyID: "tidal:2", ISRC: "AAA111", Name: "Duplicate"}, {ProviderID: "tidal", SpotifyID: "tidal:3", ISRC: "BBB222", Name: "Second"}, }, nil - case "deezer": - return []ExtTrackMetadata{ - {ProviderID: "deezer", SpotifyID: "deezer:4", ISRC: "CCC333", Name: "Third"}, - }, nil default: return nil, nil } @@ -237,13 +233,13 @@ func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) { if err != nil { t.Fatalf("SearchTracksWithMetadataProviders returned error: %v", err) } - if len(tracks) != 3 { - t.Fatalf("unexpected track count: got %d want 3", len(tracks)) + if len(tracks) != 2 { + t.Fatalf("unexpected track count: got %d want 2", len(tracks)) } - if tracks[0].ProviderID != "qobuz" || tracks[1].ProviderID != "tidal" || tracks[2].ProviderID != "deezer" { + if tracks[0].ProviderID != "qobuz" || tracks[1].ProviderID != "tidal" { t.Fatalf("unexpected track provider order: %+v", tracks) } - if len(calls) != 3 || calls[0] != "qobuz" || calls[1] != "tidal" || calls[2] != "deezer" { + if len(calls) != 2 || calls[0] != "qobuz" || calls[1] != "tidal" { t.Fatalf("unexpected provider call order: %v", calls) } } diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index cc5ef880..1b2bf496 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -371,16 +371,6 @@ import Gobackend // Import Go framework if let error = error { throw error } return response - case "searchDeezerAll": - let args = call.arguments as! [String: Any] - let query = args["query"] as! String - let trackLimit = args["track_limit"] as? Int ?? 15 - let artistLimit = args["artist_limit"] as? Int ?? 3 - let filter = args["filter"] as? String ?? "" - let response = GobackendSearchDeezerAll(query, Int(trackLimit), Int(artistLimit), filter, &error) - if let error = error { throw error } - return response - case "searchTidalAll": let args = call.arguments as! [String: Any] let query = args["query"] as! String diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index 7a2a1613..ba77fdf8 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -481,6 +481,7 @@ class ExtensionState { } class ExtensionNotifier extends Notifier { + static const _builtInMetadataProviders = ['qobuz', 'tidal']; AppLifecycleListener? _appLifecycleListener; bool _cleanupInFlight = false; Completer? _initializationCompleter; @@ -883,10 +884,15 @@ class ExtensionNotifier extends Notifier { ); await PlatformBridge.setMetadataProviderPriority(priority); } else { - priority = _sanitizeMetadataProviderPriority( - await PlatformBridge.getMetadataProviderPriority(), - ); + final backendPriority = + await PlatformBridge.getMetadataProviderPriority(); + priority = _sanitizeMetadataProviderPriority(backendPriority); _log.d('Using default metadata provider priority: $priority'); + await prefs.setString( + _metadataProviderPriorityKey, + jsonEncode(priority), + ); + await PlatformBridge.setMetadataProviderPriority(priority); } state = state.copyWith(metadataProviderPriority: priority); @@ -942,17 +948,26 @@ class ExtensionNotifier extends Notifier { } List getAllMetadataProviders() { - final providers = ['deezer', 'qobuz', 'tidal']; - for (final ext in state.extensions) { - if (ext.enabled && ext.hasMetadataProvider) { - providers.add(ext.id); - } - } - return providers; + final metadataExtensions = state.extensions + .where((ext) => ext.enabled && ext.hasMetadataProvider) + .toList(); + final primarySearchMetadataExtensions = metadataExtensions + .where((ext) => ext.searchBehavior?.primary == true) + .map((ext) => ext.id); + final otherMetadataExtensions = metadataExtensions + .where((ext) => ext.searchBehavior?.primary != true) + .map((ext) => ext.id); + + return [ + ...primarySearchMetadataExtensions, + ..._builtInMetadataProviders, + ...otherMetadataExtensions, + ]; } List _sanitizeMetadataProviderPriority(List input) { final allowed = getAllMetadataProviders().toSet(); + final preferredOrder = getAllMetadataProviders(); final result = []; for (final provider in input) { @@ -961,7 +976,18 @@ class ExtensionNotifier extends Notifier { } } - for (final provider in const ['deezer', 'qobuz', 'tidal']) { + final hasPreferredExtension = preferredOrder.any( + (provider) => !_builtInMetadataProviders.contains(provider), + ); + final hasSavedExtension = result.any( + (provider) => !_builtInMetadataProviders.contains(provider), + ); + + if (!hasSavedExtension && hasPreferredExtension) { + return List.from(preferredOrder); + } + + for (final provider in preferredOrder) { if (!result.contains(provider)) { result.add(provider); } diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 402cdfd7..a02d592d 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -583,8 +583,97 @@ class TrackNotifier extends Notifier { String? builtInSearchProvider, }) async { final requestId = ++_currentRequestId; - final currentFilter = filterOverride ?? state.selectedSearchFilter; + final settings = ref.read(settingsProvider); + final extensionState = ref.read(extensionProvider); + + String? resolvedProvider = builtInSearchProvider; + if (resolvedProvider == null || resolvedProvider.isEmpty) { + final explicitProvider = settings.searchProvider?.trim(); + if (explicitProvider != null && explicitProvider.isNotEmpty) { + resolvedProvider = explicitProvider; + } else { + resolvedProvider = + extensionState.extensions + .where( + (ext) => + ext.enabled && + ext.hasCustomSearch && + ext.searchBehavior?.primary == true, + ) + .map((ext) => ext.id) + .firstOrNull ?? + extensionState.extensions + .where((ext) => ext.enabled && ext.hasCustomSearch) + .map((ext) => ext.id) + .firstOrNull; + } + } + + final isEnabledExtensionProvider = + resolvedProvider != null && + resolvedProvider.isNotEmpty && + extensionState.extensions.any( + (ext) => ext.enabled && ext.id == resolvedProvider, + ); + + if (resolvedProvider != null && + resolvedProvider.isNotEmpty && + resolvedProvider != 'tidal' && + resolvedProvider != 'qobuz' && + !isEnabledExtensionProvider && + settings.searchProvider?.trim() == resolvedProvider) { + ref.read(settingsProvider.notifier).setSearchProvider(null); + resolvedProvider = + extensionState.extensions + .where( + (ext) => + ext.enabled && + ext.hasCustomSearch && + ext.searchBehavior?.primary == true, + ) + .map((ext) => ext.id) + .firstOrNull ?? + extensionState.extensions + .where((ext) => ext.enabled && ext.hasCustomSearch) + .map((ext) => ext.id) + .firstOrNull; + } + + if (resolvedProvider != null && + resolvedProvider.isNotEmpty && + resolvedProvider != 'tidal' && + resolvedProvider != 'qobuz' && + extensionState.extensions.any( + (ext) => ext.enabled && ext.id == resolvedProvider, + )) { + final resolvedFilter = currentFilter ?? 'track'; + Map? options; + options = {'filter': resolvedFilter}; + await customSearch( + resolvedProvider, + query, + options: options, + selectedFilter: resolvedFilter, + ); + return; + } + + final effectiveBuiltInProvider = + resolvedProvider == 'tidal' || resolvedProvider == 'qobuz' + ? resolvedProvider + : builtInSearchProvider; + + if (effectiveBuiltInProvider == null || effectiveBuiltInProvider.isEmpty) { + state = TrackState( + isLoading: false, + error: 'No active search provider available', + hasSearchText: state.hasSearchText, + isShowingRecentAccess: state.isShowingRecentAccess, + selectedSearchFilter: currentFilter, + ); + return; + } state = TrackState( isLoading: true, @@ -594,15 +683,13 @@ class TrackNotifier extends Notifier { ); try { - final settings = ref.read(settingsProvider); - final extensionState = ref.read(extensionProvider); final hasActiveMetadataExtensions = extensionState.extensions.any( (e) => e.enabled && e.hasMetadataProvider, ); final includeExtensions = settings.useExtensionProviders && hasActiveMetadataExtensions; - final effectiveProvider = builtInSearchProvider ?? 'deezer'; + final effectiveProvider = effectiveBuiltInProvider; _log.i( 'Search started: provider=$effectiveProvider, query="$query", includeExtensions=$includeExtensions, filter=$currentFilter', @@ -611,25 +698,6 @@ class TrackNotifier extends Notifier { Map results; List> metadataTrackResults = []; - if (effectiveProvider == 'deezer') { - try { - _log.d('Calling metadata provider search API...'); - metadataTrackResults = - await PlatformBridge.searchTracksWithMetadataProviders( - query, - limit: 20, - includeExtensions: includeExtensions, - ); - _log.i( - 'Metadata providers returned ${metadataTrackResults.length} tracks', - ); - } catch (e) { - _log.w( - 'Metadata provider search failed, falling back to Deezer tracks: $e', - ); - } - } - switch (effectiveProvider) { case 'tidal': _log.d('Calling Tidal search API...'); @@ -650,13 +718,19 @@ class TrackNotifier extends Notifier { ); break; default: - _log.d('Calling Deezer search API...'); - results = await PlatformBridge.searchDeezerAll( - query, - trackLimit: 20, - artistLimit: 2, - filter: currentFilter, - ); + _log.d('Calling metadata provider track search API...'); + metadataTrackResults = + await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 20, + includeExtensions: includeExtensions, + ); + results = const >{ + 'tracks': [], + 'artists': [], + 'albums': [], + 'playlists': [], + }; break; } _log.i( diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index af7b0d43..912b8ebd 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -426,14 +426,18 @@ class _HomeTabState extends ConsumerState String? currentSearchProvider, List extensions, ) { + final resolvedSearchProvider = _resolveSearchProvider( + currentSearchProvider, + extensions, + ); final isUsingExtensionSearch = - currentSearchProvider != null && - currentSearchProvider.isNotEmpty && - extensions.any((e) => e.id == currentSearchProvider && e.enabled); + resolvedSearchProvider != null && + resolvedSearchProvider.isNotEmpty && + extensions.any((e) => e.id == resolvedSearchProvider && e.enabled); if (isUsingExtensionSearch) { final currentSearchExtension = extensions - .where((e) => e.id == currentSearchProvider && e.enabled) + .where((e) => e.id == resolvedSearchProvider && e.enabled) .firstOrNull; final filters = currentSearchExtension?.searchBehavior?.filters; if (filters != null && filters.isNotEmpty) { @@ -449,6 +453,36 @@ class _HomeTabState extends ConsumerState ]; } + Extension? _defaultSearchExtension(List extensions) { + return extensions + .where( + (ext) => + ext.enabled && + ext.hasCustomSearch && + ext.searchBehavior?.primary == true, + ) + .firstOrNull ?? + extensions + .where((ext) => ext.enabled && ext.hasCustomSearch) + .firstOrNull; + } + + String? _resolveSearchProvider( + String? explicitSearchProvider, + List extensions, + ) { + final explicit = explicitSearchProvider?.trim(); + if (explicit != null && + explicit.isNotEmpty && + (_builtInSearchProviders.contains(explicit) || + extensions.any( + (ext) => ext.enabled && ext.hasCustomSearch && ext.id == explicit, + ))) { + return explicit; + } + return _defaultSearchExtension(extensions)?.id; + } + String? _sanitizeSearchFilterForProvider( String? filter, String? currentSearchProvider, @@ -585,7 +619,10 @@ class _HomeTabState extends ConsumerState bool _isLiveSearchEnabled() { final settings = ref.read(settingsProvider); final extState = ref.read(extensionProvider); - final searchProvider = settings.searchProvider; + final searchProvider = _resolveSearchProvider( + settings.searchProvider, + extState.extensions, + ); if (searchProvider == null || searchProvider.isEmpty) return false; @@ -654,7 +691,10 @@ class _HomeTabState extends ConsumerState Future _performSearch(String query, {String? filterOverride}) async { final settings = ref.read(settingsProvider); final extState = ref.read(extensionProvider); - final searchProvider = settings.searchProvider; + final searchProvider = _resolveSearchProvider( + settings.searchProvider, + extState.extensions, + ); final selectedFilter = _sanitizeSearchFilterForProvider( filterOverride, @@ -2166,17 +2206,25 @@ class _HomeTabState extends ConsumerState ); } + bool _isEnabledMetadataExtension(String? providerId) { + final normalized = providerId?.trim(); + if (normalized == null || normalized.isEmpty) return false; + + return ref + .read(extensionProvider) + .extensions + .any( + (ext) => + ext.enabled && ext.hasMetadataProvider && ext.id == normalized, + ); + } + void _navigateToRecentItem(RecentAccessItem item) { _searchFocusNode.unfocus(); switch (item.type) { case RecentAccessType.artist: - if (item.providerId != null && - item.providerId!.isNotEmpty && - item.providerId != 'deezer' && - item.providerId != 'spotify' && - item.providerId != 'tidal' && - item.providerId != 'qobuz') { + if (_isEnabledMetadataExtension(item.providerId)) { Navigator.push( context, MaterialPageRoute( @@ -2213,12 +2261,7 @@ class _HomeTabState extends ConsumerState ), ), ); - } else if (item.providerId != null && - item.providerId!.isNotEmpty && - item.providerId != 'deezer' && - item.providerId != 'spotify' && - item.providerId != 'tidal' && - item.providerId != 'qobuz') { + } else if (_isEnabledMetadataExtension(item.providerId)) { Navigator.push( context, MaterialPageRoute( @@ -2263,12 +2306,7 @@ class _HomeTabState extends ConsumerState return; } - if (item.providerId != null && - item.providerId!.isNotEmpty && - item.providerId != 'deezer' && - item.providerId != 'spotify' && - item.providerId != 'tidal' && - item.providerId != 'qobuz') { + if (_isEnabledMetadataExtension(item.providerId)) { Navigator.push( context, MaterialPageRoute( @@ -3185,8 +3223,11 @@ class _HomeTabState extends ConsumerState String _getSearchHint() { final settings = ref.read(settingsProvider); - final searchProvider = settings.searchProvider; final extState = ref.read(extensionProvider); + final searchProvider = _resolveSearchProvider( + settings.searchProvider, + extState.extensions, + ); if (!extState.isInitialized) { return 'Paste supported URL or search...'; @@ -3387,9 +3428,23 @@ class _SearchProviderDropdown extends ConsumerWidget { const _SearchProviderDropdown({this.onProviderChanged}); + Extension? _defaultSearchExtension(List extensions) { + return extensions + .where( + (ext) => + ext.enabled && + ext.hasCustomSearch && + ext.searchBehavior?.primary == true, + ) + .firstOrNull ?? + extensions + .where((ext) => ext.enabled && ext.hasCustomSearch) + .firstOrNull; + } + @override Widget build(BuildContext context, WidgetRef ref) { - final currentProvider = ref.watch( + final rawCurrentProvider = ref.watch( settingsProvider.select((s) => s.searchProvider), ); final extensions = ref.watch(extensionProvider.select((s) => s.extensions)); @@ -3398,6 +3453,17 @@ class _SearchProviderDropdown extends ConsumerWidget { final searchProviders = extensions .where((ext) => ext.enabled && ext.hasCustomSearch) .toList(); + final primarySearchExtension = _defaultSearchExtension(searchProviders); + final defaultProviderLabel = + primarySearchExtension?.displayName ?? 'Deezer'; + final defaultProviderIconPath = primarySearchExtension?.iconPath; + final currentProvider = + rawCurrentProvider != null && + rawCurrentProvider.isNotEmpty && + ({'tidal', 'qobuz'}.contains(rawCurrentProvider) || + searchProviders.any((e) => e.id == rawCurrentProvider)) + ? rawCurrentProvider + : null; Extension? currentExt; if (currentProvider != null && currentProvider.isNotEmpty) { @@ -3417,6 +3483,19 @@ class _SearchProviderDropdown extends ConsumerWidget { if (currentExt.searchBehavior?.icon != null) { displayIcon = _getIconFromName(currentExt.searchBehavior!.icon!); } + } else if (primarySearchExtension?.searchBehavior?.icon != null) { + displayIcon = _getIconFromName( + primarySearchExtension!.searchBehavior!.icon!, + ); + iconPath = defaultProviderIconPath; + } else if (defaultProviderIconPath != null && + defaultProviderIconPath.isNotEmpty) { + iconPath = defaultProviderIconPath; + if (primarySearchExtension?.searchBehavior?.icon != null) { + displayIcon = _getIconFromName( + primarySearchExtension!.searchBehavior!.icon!, + ); + } } else if (isBuiltInProvider) { displayIcon = Icons.music_note; } @@ -3471,7 +3550,7 @@ class _SearchProviderDropdown extends ConsumerWidget { const SizedBox(width: 12), Expanded( child: Text( - 'Deezer', + defaultProviderLabel, style: TextStyle( fontWeight: currentProvider == null || currentProvider.isEmpty diff --git a/lib/screens/settings/metadata_provider_priority_page.dart b/lib/screens/settings/metadata_provider_priority_page.dart index ba66489c..853e3916 100644 --- a/lib/screens/settings/metadata_provider_priority_page.dart +++ b/lib/screens/settings/metadata_provider_priority_page.dart @@ -225,8 +225,8 @@ class _MetadataProviderItem extends StatelessWidget { return _MetadataProviderInfo( name: 'Deezer', icon: Icons.album, - description: context.l10n.metadataNoRateLimits, - isBuiltIn: true, + description: context.l10n.providerExtension, + isBuiltIn: false, ); case 'qobuz': return _MetadataProviderInfo( diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index 8ead2d5b..481ec181 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -719,13 +719,39 @@ class _MetadataSourceSelector extends ConsumerWidget { static const _builtInProviders = {'tidal': 'Tidal', 'qobuz': 'Qobuz'}; + Extension? _defaultSearchExtension(List extensions) { + return extensions + .where( + (ext) => + ext.enabled && + ext.hasCustomSearch && + ext.searchBehavior?.primary == true, + ) + .firstOrNull ?? + extensions + .where((ext) => ext.enabled && ext.hasCustomSearch) + .firstOrNull; + } + @override Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; final settings = ref.watch(settingsProvider); final extState = ref.watch(extensionProvider); - final searchProvider = settings.searchProvider ?? ''; + final rawSearchProvider = settings.searchProvider?.trim() ?? ''; + final isValidBuiltIn = _builtInProviders.containsKey(rawSearchProvider); + final primarySearchExtension = _defaultSearchExtension(extState.extensions); + final defaultProviderLabel = + primarySearchExtension?.displayName ?? 'Deezer'; + final searchProvider = + isValidBuiltIn || + extState.extensions.any( + (e) => + e.enabled && e.hasCustomSearch && e.id == rawSearchProvider, + ) + ? rawSearchProvider + : ''; final isBuiltIn = _builtInProviders.containsKey(searchProvider); Extension? activeExtension; @@ -772,7 +798,7 @@ class _MetadataSourceSelector extends ConsumerWidget { children: [ _SourceChip( icon: Icons.graphic_eq, - label: 'Deezer', + label: defaultProviderLabel, isSelected: searchProvider.isEmpty, onTap: () { if (hasNonDefaultProvider) { @@ -816,7 +842,7 @@ class _MetadataSourceSelector extends ConsumerWidget { const SizedBox(width: 8), Expanded( child: Text( - 'Tap Deezer to switch back from extension', + 'Tap $defaultProviderLabel to switch back from extension', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), diff --git a/lib/services/csv_import_service.dart b/lib/services/csv_import_service.dart index 45d59d18..7be9fcbd 100644 --- a/lib/services/csv_import_service.dart +++ b/lib/services/csv_import_service.dart @@ -61,32 +61,29 @@ class CsvImportService { if (trackData == null) { try { final query = '${track.artistName} ${track.name}'; - final searchResult = await PlatformBridge.searchDeezerAll( + final searchResult = await PlatformBridge.customSearchWithExtension( + 'deezer', query, - trackLimit: 5, + options: {'filter': 'track', 'limit': 5}, ); - if (searchResult.containsKey('tracks')) { - final tracksList = searchResult['tracks'] as List?; - if (tracksList != null && tracksList.isNotEmpty) { - for (final result in tracksList) { - final resultMap = result as Map; - final resultName = - (resultMap['name'] as String?)?.toLowerCase() ?? ''; - final trackNameLower = track.name.toLowerCase(); + if (searchResult.isNotEmpty) { + for (final resultMap in searchResult) { + final resultName = + (resultMap['name'] as String?)?.toLowerCase() ?? ''; + final trackNameLower = track.name.toLowerCase(); - if (resultName.contains(trackNameLower) || - trackNameLower.contains(resultName)) { - trackData = resultMap; - _log.d('Text search match for ${track.name}: $resultName'); - break; - } + if (resultName.contains(trackNameLower) || + trackNameLower.contains(resultName)) { + trackData = resultMap; + _log.d('Text search match for ${track.name}: $resultName'); + break; } + } - if (trackData == null && tracksList.isNotEmpty) { - trackData = tracksList.first as Map; - _log.d('Using first search result for ${track.name}'); - } + if (trackData == null) { + trackData = searchResult.first; + _log.d('Using first search result for ${track.name}'); } } } catch (e) { diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 19a96a5a..b88174fb 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -496,21 +496,6 @@ class PlatformBridge { await _channel.invokeMethod('clearTrackCache'); } - static Future> searchDeezerAll( - String query, { - int trackLimit = 15, - int artistLimit = 2, - String? filter, - }) async { - final result = await _channel.invokeMethod('searchDeezerAll', { - 'query': query, - 'track_limit': trackLimit, - 'artist_limit': artistLimit, - 'filter': filter ?? '', - }); - return jsonDecode(result as String) as Map; - } - static Future> searchTidalAll( String query, { int trackLimit = 15, diff --git a/lib/utils/clickable_metadata.dart b/lib/utils/clickable_metadata.dart index 1ec5879a..c2e2cc29 100644 --- a/lib/utils/clickable_metadata.dart +++ b/lib/utils/clickable_metadata.dart @@ -10,6 +10,19 @@ import 'package:spotiflac_android/utils/artist_utils.dart'; import 'package:spotiflac_android/utils/logger.dart'; final _log = AppLogger('ClickableMetadata'); +const _deezerExtensionId = 'deezer'; + +Future>> _searchDeezerExtension( + String query, { + required String filter, + int limit = 5, +}) { + return PlatformBridge.customSearchWithExtension( + _deezerExtensionId, + query, + options: {'filter': filter, 'limit': limit}, + ); +} Future navigateToArtist( BuildContext context, { @@ -39,15 +52,14 @@ Future navigateToArtist( _showLoadingSnackBar(context, 'Looking up artist...'); try { - final results = await PlatformBridge.searchDeezerAll( + final artistList = await _searchDeezerExtension( artistName, - trackLimit: 0, - artistLimit: 3, + filter: 'artist', + limit: 3, ); if (!context.mounted) return; ScaffoldMessenger.of(context).hideCurrentSnackBar(); - final artistList = results['artists'] as List? ?? []; if (artistList.isEmpty) { _showUnavailable(context, 'Artist'); return; @@ -56,15 +68,13 @@ Future navigateToArtist( Map? bestMatch; final lowerName = artistName.toLowerCase().trim(); for (final a in artistList) { - if (a is Map) { - final name = (a['name'] as String? ?? '').toLowerCase().trim(); - if (name == lowerName) { - bestMatch = a; - break; - } + final name = (a['name'] as String? ?? '').toLowerCase().trim(); + if (name == lowerName) { + bestMatch = a; + break; } } - bestMatch ??= artistList.first as Map; + bestMatch ??= artistList.first; final resolvedId = bestMatch['id'] as String? ?? ''; final resolvedName = bestMatch['name'] as String? ?? artistName; @@ -81,6 +91,7 @@ Future navigateToArtist( artistId: resolvedId, artistName: resolvedName, coverUrl: resolvedImage ?? coverUrl, + extensionId: _deezerExtensionId, ); } catch (e) { _log.e('Failed to look up artist "$artistName": $e', e); @@ -125,15 +136,14 @@ Future navigateToAlbum( ? '$albumName $artistName' : albumName; - final results = await PlatformBridge.searchDeezerAll( + final albumList = await _searchDeezerExtension( query, - trackLimit: 0, - artistLimit: 0, + filter: 'album', + limit: 5, ); if (!context.mounted) return; ScaffoldMessenger.of(context).hideCurrentSnackBar(); - final albumList = results['albums'] as List? ?? []; if (albumList.isEmpty) { _showUnavailable(context, 'Album'); return; @@ -142,15 +152,13 @@ Future navigateToAlbum( Map? bestMatch; final lowerName = albumName.toLowerCase().trim(); for (final a in albumList) { - if (a is Map) { - final name = (a['name'] as String? ?? '').toLowerCase().trim(); - if (name == lowerName) { - bestMatch = a; - break; - } + final name = (a['name'] as String? ?? '').toLowerCase().trim(); + if (name == lowerName) { + bestMatch = a; + break; } } - bestMatch ??= albumList.first as Map; + bestMatch ??= albumList.first; final resolvedId = bestMatch['id'] as String? ?? ''; final resolvedName = bestMatch['name'] as String? ?? albumName; @@ -167,6 +175,7 @@ Future navigateToAlbum( albumId: resolvedId, albumName: resolvedName, coverUrl: resolvedImage ?? coverUrl, + extensionId: _deezerExtensionId, ); } catch (e) { _log.e('Failed to look up album "$albumName": $e', e); @@ -207,7 +216,7 @@ void _pushAlbumScreen( String? coverUrl, String? extensionId, }) { - const builtInProviders = {'tidal', 'qobuz', 'deezer'}; + const builtInProviders = {'tidal', 'qobuz'}; final isExtension = extensionId != null && !builtInProviders.contains(extensionId); final resolvedExtensionId = extensionId;