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 e76f8cb9..88e7ce4a 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -409,9 +409,10 @@ class MainActivity: FlutterActivity() { "searchDeezerAll" -> { val query = call.argument("query") ?: "" val trackLimit = call.argument("track_limit") ?: 15 - val artistLimit = call.argument("artist_limit") ?: 3 + val artistLimit = call.argument("artist_limit") ?: 2 + val filter = call.argument("filter") ?: "" val response = withContext(Dispatchers.IO) { - Gobackend.searchDeezerAll(query, trackLimit.toLong(), artistLimit.toLong()) + Gobackend.searchDeezerAll(query, trackLimit.toLong(), artistLimit.toLong(), filter) } result.success(response) } diff --git a/go_backend/deezer.go b/go_backend/deezer.go index 64004026..38a583d9 100644 --- a/go_backend/deezer.go +++ b/go_backend/deezer.go @@ -183,12 +183,40 @@ type deezerPlaylistFull struct { } // NOTE: ISRC is NOT fetched during search for performance - use GetTrackISRC when needed for download -func (c *DeezerClient) SearchAll(ctx context.Context, query string, trackLimit, artistLimit int) (*SearchAllResult, error) { - GoLog("[Deezer] SearchAll: query=%q, trackLimit=%d, artistLimit=%d\n", query, trackLimit, artistLimit) +// filter can be: "" (all), "track", "artist", "album", "playlist" +func (c *DeezerClient) SearchAll(ctx context.Context, query string, trackLimit, artistLimit int, filter string) (*SearchAllResult, error) { + GoLog("[Deezer] SearchAll: query=%q, trackLimit=%d, artistLimit=%d, filter=%q\n", query, trackLimit, artistLimit, filter) albumLimit := 5 // Same as artistLimit for consistency playlistLimit := 5 - cacheKey := fmt.Sprintf("deezer:all:%s:%d:%d:%d:%d", query, trackLimit, artistLimit, albumLimit, playlistLimit) + + // When filter is specified, increase limits for that type only + if filter != "" { + switch filter { + case "track": + trackLimit = 50 + artistLimit = 0 + albumLimit = 0 + playlistLimit = 0 + case "artist": + trackLimit = 0 + artistLimit = 20 + albumLimit = 0 + playlistLimit = 0 + case "album": + trackLimit = 0 + artistLimit = 0 + albumLimit = 20 + playlistLimit = 0 + case "playlist": + trackLimit = 0 + artistLimit = 0 + albumLimit = 0 + playlistLimit = 20 + } + } + + cacheKey := fmt.Sprintf("deezer:all:%s:%d:%d:%d:%d:%s", query, trackLimit, artistLimit, albumLimit, playlistLimit, filter) c.cacheMu.RLock() if entry, ok := c.searchCache[cacheKey]; ok && !entry.isExpired() { @@ -206,175 +234,183 @@ func (c *DeezerClient) SearchAll(ctx context.Context, query string, trackLimit, } // Search tracks - NO ISRC fetch for performance - trackURL := fmt.Sprintf("%s/track?q=%s&limit=%d", deezerSearchURL, url.QueryEscape(query), trackLimit) - GoLog("[Deezer] Fetching tracks from: %s\n", trackURL) + if trackLimit > 0 { + trackURL := fmt.Sprintf("%s/track?q=%s&limit=%d", deezerSearchURL, url.QueryEscape(query), trackLimit) + GoLog("[Deezer] Fetching tracks from: %s\n", trackURL) - var trackResp struct { - Data []deezerTrack `json:"data"` - Error *struct { - Type string `json:"type"` - Message string `json:"message"` - Code int `json:"code"` - } `json:"error"` - } - if err := c.getJSON(ctx, trackURL, &trackResp); err != nil { - GoLog("[Deezer] Track search failed: %v\n", err) - return nil, fmt.Errorf("deezer track search failed: %w", err) - } + var trackResp struct { + Data []deezerTrack `json:"data"` + Error *struct { + Type string `json:"type"` + Message string `json:"message"` + Code int `json:"code"` + } `json:"error"` + } + if err := c.getJSON(ctx, trackURL, &trackResp); err != nil { + GoLog("[Deezer] Track search failed: %v\n", err) + return nil, fmt.Errorf("deezer track search failed: %w", err) + } - if trackResp.Error != nil { - GoLog("[Deezer] API error: type=%s, code=%d, message=%s\n", trackResp.Error.Type, trackResp.Error.Code, trackResp.Error.Message) - return nil, fmt.Errorf("deezer API error: %s (code %d)", trackResp.Error.Message, trackResp.Error.Code) - } + if trackResp.Error != nil { + GoLog("[Deezer] API error: type=%s, code=%d, message=%s\n", trackResp.Error.Type, trackResp.Error.Code, trackResp.Error.Message) + return nil, fmt.Errorf("deezer API error: %s (code %d)", trackResp.Error.Message, trackResp.Error.Code) + } - GoLog("[Deezer] Got %d tracks from API\n", len(trackResp.Data)) + GoLog("[Deezer] Got %d tracks from API\n", len(trackResp.Data)) - for _, track := range trackResp.Data { - result.Tracks = append(result.Tracks, c.convertTrack(track)) + for _, track := range trackResp.Data { + result.Tracks = append(result.Tracks, c.convertTrack(track)) + } } // Search artists - artistURL := fmt.Sprintf("%s/artist?q=%s&limit=%d", deezerSearchURL, url.QueryEscape(query), artistLimit) - GoLog("[Deezer] Fetching artists from: %s\n", artistURL) + if artistLimit > 0 { + artistURL := fmt.Sprintf("%s/artist?q=%s&limit=%d", deezerSearchURL, url.QueryEscape(query), artistLimit) + GoLog("[Deezer] Fetching artists from: %s\n", artistURL) - var artistResp struct { - Data []deezerArtist `json:"data"` - Error *struct { - Type string `json:"type"` - Message string `json:"message"` - Code int `json:"code"` - } `json:"error"` - } - if err := c.getJSON(ctx, artistURL, &artistResp); err == nil { - if artistResp.Error != nil { - GoLog("[Deezer] Artist API error: type=%s, code=%d, message=%s\n", artistResp.Error.Type, artistResp.Error.Code, artistResp.Error.Message) - } else { - GoLog("[Deezer] Got %d artists from API\n", len(artistResp.Data)) - for _, artist := range artistResp.Data { - result.Artists = append(result.Artists, SearchArtistResult{ - ID: fmt.Sprintf("deezer:%d", artist.ID), - Name: artist.Name, - Images: c.getBestArtistImage(artist), - Followers: artist.NbFan, - Popularity: 0, - }) - } + var artistResp struct { + Data []deezerArtist `json:"data"` + Error *struct { + Type string `json:"type"` + Message string `json:"message"` + Code int `json:"code"` + } `json:"error"` + } + if err := c.getJSON(ctx, artistURL, &artistResp); err == nil { + if artistResp.Error != nil { + GoLog("[Deezer] Artist API error: type=%s, code=%d, message=%s\n", artistResp.Error.Type, artistResp.Error.Code, artistResp.Error.Message) + } else { + GoLog("[Deezer] Got %d artists from API\n", len(artistResp.Data)) + for _, artist := range artistResp.Data { + result.Artists = append(result.Artists, SearchArtistResult{ + ID: fmt.Sprintf("deezer:%d", artist.ID), + Name: artist.Name, + Images: c.getBestArtistImage(artist), + Followers: artist.NbFan, + Popularity: 0, + }) + } + } + } else { + GoLog("[Deezer] Artist search failed: %v\n", err) } - } else { - GoLog("[Deezer] Artist search failed: %v\n", err) } // Search albums - albumURL := fmt.Sprintf("%s/album?q=%s&limit=%d", deezerSearchURL, url.QueryEscape(query), albumLimit) - GoLog("[Deezer] Fetching albums from: %s\n", albumURL) + if albumLimit > 0 { + albumURL := fmt.Sprintf("%s/album?q=%s&limit=%d", deezerSearchURL, url.QueryEscape(query), albumLimit) + GoLog("[Deezer] Fetching albums from: %s\n", albumURL) - var albumResp struct { - Data []struct { - ID int64 `json:"id"` - Title string `json:"title"` - Cover string `json:"cover"` - CoverMedium string `json:"cover_medium"` - CoverBig string `json:"cover_big"` - CoverXL string `json:"cover_xl"` - NbTracks int `json:"nb_tracks"` - ReleaseDate string `json:"release_date"` - RecordType string `json:"record_type"` - Artist deezerArtist `json:"artist"` - } `json:"data"` - Error *struct { - Type string `json:"type"` - Message string `json:"message"` - Code int `json:"code"` - } `json:"error"` - } - if err := c.getJSON(ctx, albumURL, &albumResp); err == nil { - if albumResp.Error != nil { - GoLog("[Deezer] Album API error: type=%s, code=%d, message=%s\n", albumResp.Error.Type, albumResp.Error.Code, albumResp.Error.Message) - } else { - GoLog("[Deezer] Got %d albums from API\n", len(albumResp.Data)) - for _, album := range albumResp.Data { - coverURL := album.CoverXL - if coverURL == "" { - coverURL = album.CoverBig - } - if coverURL == "" { - coverURL = album.CoverMedium - } - if coverURL == "" { - coverURL = album.Cover - } - - albumType := album.RecordType - if albumType == "compile" { - albumType = "compilation" - } - - result.Albums = append(result.Albums, SearchAlbumResult{ - ID: fmt.Sprintf("deezer:%d", album.ID), - Name: album.Title, - Artists: album.Artist.Name, - Images: coverURL, - ReleaseDate: album.ReleaseDate, - TotalTracks: album.NbTracks, - AlbumType: albumType, - }) - } + var albumResp struct { + Data []struct { + ID int64 `json:"id"` + Title string `json:"title"` + Cover string `json:"cover"` + CoverMedium string `json:"cover_medium"` + CoverBig string `json:"cover_big"` + CoverXL string `json:"cover_xl"` + NbTracks int `json:"nb_tracks"` + ReleaseDate string `json:"release_date"` + RecordType string `json:"record_type"` + Artist deezerArtist `json:"artist"` + } `json:"data"` + Error *struct { + Type string `json:"type"` + Message string `json:"message"` + Code int `json:"code"` + } `json:"error"` + } + if err := c.getJSON(ctx, albumURL, &albumResp); err == nil { + if albumResp.Error != nil { + GoLog("[Deezer] Album API error: type=%s, code=%d, message=%s\n", albumResp.Error.Type, albumResp.Error.Code, albumResp.Error.Message) + } else { + GoLog("[Deezer] Got %d albums from API\n", len(albumResp.Data)) + for _, album := range albumResp.Data { + coverURL := album.CoverXL + if coverURL == "" { + coverURL = album.CoverBig + } + if coverURL == "" { + coverURL = album.CoverMedium + } + if coverURL == "" { + coverURL = album.Cover + } + + albumType := album.RecordType + if albumType == "compile" { + albumType = "compilation" + } + + result.Albums = append(result.Albums, SearchAlbumResult{ + ID: fmt.Sprintf("deezer:%d", album.ID), + Name: album.Title, + Artists: album.Artist.Name, + Images: coverURL, + ReleaseDate: album.ReleaseDate, + TotalTracks: album.NbTracks, + AlbumType: albumType, + }) + } + } + } else { + GoLog("[Deezer] Album search failed: %v\n", err) } - } else { - GoLog("[Deezer] Album search failed: %v\n", err) } // Search playlists - playlistURL := fmt.Sprintf("%s/playlist?q=%s&limit=%d", deezerSearchURL, url.QueryEscape(query), playlistLimit) - GoLog("[Deezer] Fetching playlists from: %s\n", playlistURL) + if playlistLimit > 0 { + playlistURL := fmt.Sprintf("%s/playlist?q=%s&limit=%d", deezerSearchURL, url.QueryEscape(query), playlistLimit) + GoLog("[Deezer] Fetching playlists from: %s\n", playlistURL) - var playlistResp struct { - Data []struct { - ID int64 `json:"id"` - Title string `json:"title"` - Picture string `json:"picture"` - PictureMedium string `json:"picture_medium"` - PictureBig string `json:"picture_big"` - PictureXL string `json:"picture_xl"` - NbTracks int `json:"nb_tracks"` - User struct { - Name string `json:"name"` - } `json:"user"` - } `json:"data"` - Error *struct { - Type string `json:"type"` - Message string `json:"message"` - Code int `json:"code"` - } `json:"error"` - } - if err := c.getJSON(ctx, playlistURL, &playlistResp); err == nil { - if playlistResp.Error != nil { - GoLog("[Deezer] Playlist API error: type=%s, code=%d, message=%s\n", playlistResp.Error.Type, playlistResp.Error.Code, playlistResp.Error.Message) - } else { - GoLog("[Deezer] Got %d playlists from API\n", len(playlistResp.Data)) - for _, playlist := range playlistResp.Data { - pictureURL := playlist.PictureXL - if pictureURL == "" { - pictureURL = playlist.PictureBig - } - if pictureURL == "" { - pictureURL = playlist.PictureMedium - } - if pictureURL == "" { - pictureURL = playlist.Picture - } - - result.Playlists = append(result.Playlists, SearchPlaylistResult{ - ID: fmt.Sprintf("deezer:%d", playlist.ID), - Name: playlist.Title, - Owner: playlist.User.Name, - Images: pictureURL, - TotalTracks: playlist.NbTracks, - }) - } + var playlistResp struct { + Data []struct { + ID int64 `json:"id"` + Title string `json:"title"` + Picture string `json:"picture"` + PictureMedium string `json:"picture_medium"` + PictureBig string `json:"picture_big"` + PictureXL string `json:"picture_xl"` + NbTracks int `json:"nb_tracks"` + User struct { + Name string `json:"name"` + } `json:"user"` + } `json:"data"` + Error *struct { + Type string `json:"type"` + Message string `json:"message"` + Code int `json:"code"` + } `json:"error"` + } + if err := c.getJSON(ctx, playlistURL, &playlistResp); err == nil { + if playlistResp.Error != nil { + GoLog("[Deezer] Playlist API error: type=%s, code=%d, message=%s\n", playlistResp.Error.Type, playlistResp.Error.Code, playlistResp.Error.Message) + } else { + GoLog("[Deezer] Got %d playlists from API\n", len(playlistResp.Data)) + for _, playlist := range playlistResp.Data { + pictureURL := playlist.PictureXL + if pictureURL == "" { + pictureURL = playlist.PictureBig + } + if pictureURL == "" { + pictureURL = playlist.PictureMedium + } + if pictureURL == "" { + pictureURL = playlist.Picture + } + + result.Playlists = append(result.Playlists, SearchPlaylistResult{ + ID: fmt.Sprintf("deezer:%d", playlist.ID), + Name: playlist.Title, + Owner: playlist.User.Name, + Images: pictureURL, + TotalTracks: playlist.NbTracks, + }) + } + } + } else { + GoLog("[Deezer] Playlist search failed: %v\n", err) } - } else { - GoLog("[Deezer] Playlist search failed: %v\n", err) } GoLog("[Deezer] SearchAll complete: %d tracks, %d artists, %d albums, %d playlists\n", len(result.Tracks), len(result.Artists), len(result.Albums), len(result.Playlists)) diff --git a/go_backend/exports.go b/go_backend/exports.go index 46236cb4..4b887db1 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -716,12 +716,12 @@ func ClearTrackIDCache() { ClearTrackCache() } -func SearchDeezerAll(query string, trackLimit, artistLimit int) (string, error) { +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) + results, err := client.SearchAll(ctx, query, trackLimit, artistLimit, filter) if err != nil { return "", err } diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 7b8289ae..3279aa79 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -317,10 +317,13 @@ class TrackNotifier extends Notifier { } } - Future search(String query, {String? metadataSource}) async { + Future search(String query, {String? metadataSource, String? filterOverride}) async { final requestId = ++_currentRequestId; + + // Preserve selected filter during loading + final currentFilter = filterOverride ?? state.selectedSearchFilter; - state = TrackState(isLoading: true, hasSearchText: state.hasSearchText); + state = TrackState(isLoading: true, hasSearchText: state.hasSearchText, selectedSearchFilter: currentFilter); try { final settings = ref.read(settingsProvider); @@ -338,7 +341,7 @@ class TrackNotifier extends Notifier { final source = metadataSource ?? 'deezer'; _log.i( - 'Search started: source=$source, query="$query", useExtensions=$useExtensions', + 'Search started: source=$source, query="$query", useExtensions=$useExtensions, filter=$currentFilter', ); Map results; @@ -364,11 +367,11 @@ class TrackNotifier extends Notifier { if (source == 'deezer') { _log.d('Calling Deezer search API...'); - results = await PlatformBridge.searchDeezerAll(query, trackLimit: 20, artistLimit: 5); + results = await PlatformBridge.searchDeezerAll(query, trackLimit: 20, artistLimit: 2, filter: currentFilter); _log.i('Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums'); } else { _log.d('Calling Spotify search API...'); - results = await PlatformBridge.searchSpotifyAll(query, trackLimit: 20, artistLimit: 5); + results = await PlatformBridge.searchSpotifyAll(query, trackLimit: 20, artistLimit: 2); _log.i('Spotify returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists'); } @@ -461,11 +464,12 @@ class TrackNotifier extends Notifier { searchPlaylists: playlists, isLoading: false, hasSearchText: state.hasSearchText, + selectedSearchFilter: currentFilter, // Preserve filter in results ); } catch (e, stackTrace) { if (!_isRequestValid(requestId)) return; _log.e('Search failed: $e', e, stackTrace); - state = TrackState(isLoading: false, error: e.toString(), hasSearchText: state.hasSearchText); + state = TrackState(isLoading: false, error: e.toString(), hasSearchText: state.hasSearchText, selectedSearchFilter: currentFilter); } } diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index e9fcd4bd..13106258 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -204,11 +204,12 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient } } - Future _performSearch(String query) async { + Future _performSearch(String query, {String? filterOverride}) async { final settings = ref.read(settingsProvider); final extState = ref.read(extensionProvider); final searchProvider = settings.searchProvider; - final selectedFilter = ref.read(trackProvider).selectedSearchFilter; + // Use filterOverride if provided, otherwise read from state + final selectedFilter = filterOverride ?? ref.read(trackProvider).selectedSearchFilter; final searchKey = '${searchProvider ?? 'default'}:$query:${selectedFilter ?? 'all'}'; if (_lastSearchQuery == searchKey) return; @@ -229,7 +230,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (searchProvider != null && searchProvider.isNotEmpty && !isExtensionEnabled) { ref.read(settingsProvider.notifier).setSearchProvider(null); } - await ref.read(trackProvider.notifier).search(query, metadataSource: settings.metadataSource); + await ref.read(trackProvider.notifier).search(query, metadataSource: settings.metadataSource, filterOverride: selectedFilter); } ref.read(settingsProvider.notifier).setHasSearchedBefore(); } @@ -510,11 +511,25 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final selectedSearchFilter = ref.watch(trackProvider.select((s) => s.selectedSearchFilter)); Extension? currentSearchExtension; List searchFilters = []; - if (currentSearchProvider != null && currentSearchProvider.isNotEmpty) { + + // Check if using extension search provider + final isUsingExtensionSearch = currentSearchProvider != null && + currentSearchProvider.isNotEmpty && + extState.extensions.any((e) => e.id == currentSearchProvider && e.enabled); + + if (isUsingExtensionSearch) { currentSearchExtension = extState.extensions.where((e) => e.id == currentSearchProvider && e.enabled).firstOrNull; if (currentSearchExtension?.searchBehavior?.filters.isNotEmpty == true) { searchFilters = currentSearchExtension!.searchBehavior!.filters; } + } else { + // Default Deezer filters + searchFilters = const [ + SearchFilter(id: 'track', label: 'Tracks', icon: 'music'), + SearchFilter(id: 'artist', label: 'Artists', icon: 'artist'), + SearchFilter(id: 'album', label: 'Albums', icon: 'album'), + SearchFilter(id: 'playlist', label: 'Playlists', icon: 'playlist'), + ]; } if (hasActualResults && isShowingRecentAccess) { @@ -2127,7 +2142,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient // Reset last search query to force new search _lastSearchQuery = null; - _performSearch(text); + _performSearch(text, filterOverride: filter); } Widget _buildSearchBar(ColorScheme colorScheme) { diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index a310b130..e404b487 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -343,11 +343,12 @@ class PlatformBridge { await _channel.invokeMethod('clearTrackCache'); } - static Future> searchDeezerAll(String query, {int trackLimit = 15, int artistLimit = 3}) async { + 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; }