From c2736a61fb7596bb8aafc353470c14f2d95403ca Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Mar 2026 00:58:07 +0700 Subject: [PATCH] refactor: remove built-in Spotify API provider, use Deezer as sole default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove all Spotify credential management (client ID/secret, secure storage) - Remove Spotify platform channel handlers from MainActivity - Remove exported Go functions: GetSpotifyMetadata, SearchSpotify, SearchSpotifyAll, GetSpotifyRelatedArtists, SetSpotifyAPICredentials - Simplify GetSpotifyMetadataWithDeezerFallback to SpotFetch-only path - Remove Spotify built-in fallback in ReEnrichFile search pipeline - Always return false from HasSpotifyCredentials; getCredentials always errors - Default metadataProviderPriority is now ['deezer'] only - Sanitize provider priority list to strip 'spotify' entries on load/save - Add migration v5 to clear saved Spotify credentials from existing installs - Remove Spotify source chip and credentials UI from options settings page - Remove metadataSource param from search() — always uses Deezer - spotify-web extension remains supported via the extension provider system --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 46 --- go_backend/exports.go | 188 +---------- go_backend/extension_providers.go | 24 +- go_backend/spotify.go | 32 +- lib/models/settings.dart | 4 +- lib/models/settings.g.dart | 2 +- lib/providers/extension_provider.dart | 43 ++- lib/providers/settings_provider.dart | 108 ++---- lib/providers/track_provider.dart | 45 +-- lib/screens/home_tab.dart | 34 +- lib/screens/search_screen.dart | 10 +- .../metadata_provider_priority_page.dart | 7 - .../settings/options_settings_page.dart | 310 +----------------- lib/services/platform_bridge.dart | 61 ---- 14 files changed, 127 insertions(+), 787 deletions(-) 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 8fe38bfc..5d5650da 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -1877,38 +1877,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - "getSpotifyMetadata" -> { - val url = call.argument("url") ?: "" - val response = withContext(Dispatchers.IO) { - Gobackend.getSpotifyMetadata(url) - } - result.success(response) - } - "searchSpotify" -> { - val query = call.argument("query") ?: "" - val limit = call.argument("limit") ?: 10 - val response = withContext(Dispatchers.IO) { - Gobackend.searchSpotify(query, limit.toLong()) - } - result.success(response) - } - "searchSpotifyAll" -> { - val query = call.argument("query") ?: "" - val trackLimit = call.argument("track_limit") ?: 15 - val artistLimit = call.argument("artist_limit") ?: 3 - val response = withContext(Dispatchers.IO) { - Gobackend.searchSpotifyAll(query, trackLimit.toLong(), artistLimit.toLong()) - } - result.success(response) - } - "getSpotifyRelatedArtists" -> { - val artistId = call.argument("artist_id") ?: "" - val limit = call.argument("limit") ?: 12 - val response = withContext(Dispatchers.IO) { - Gobackend.getSpotifyRelatedArtists(artistId, limit.toLong()) - } - result.success(response) - } "checkAvailability" -> { val spotifyId = call.argument("spotify_id") ?: "" val isrc = call.argument("isrc") ?: "" @@ -2542,20 +2510,6 @@ class MainActivity: FlutterFragmentActivity() { "isDownloadServiceRunning" -> { result.success(DownloadService.isServiceRunning()) } - "setSpotifyCredentials" -> { - val clientId = call.argument("client_id") ?: "" - val clientSecret = call.argument("client_secret") ?: "" - withContext(Dispatchers.IO) { - Gobackend.setSpotifyAPICredentials(clientId, clientSecret) - } - result.success(null) - } - "hasSpotifyCredentials" -> { - val hasCredentials = withContext(Dispatchers.IO) { - Gobackend.checkSpotifyCredentials() - } - result.success(hasCredentials) - } "preWarmTrackCache" -> { val tracksJson = call.argument("tracks") ?: "[]" withContext(Dispatchers.IO) { diff --git a/go_backend/exports.go b/go_backend/exports.go index 5f83330a..00791180 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -32,126 +32,6 @@ func ParseSpotifyURL(url string) (string, error) { return string(jsonBytes), nil } -func SetSpotifyAPICredentials(clientID, clientSecret string) { - SetSpotifyCredentials(clientID, clientSecret) -} - -func CheckSpotifyCredentials() bool { - return HasSpotifyCredentials() -} - -func GetSpotifyMetadata(spotifyURL string) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - client, err := NewSpotifyMetadataClient() - if err != nil { - if shouldTrySpotFetchFallback(err) { - data, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL) - if apiErr == nil { - jsonBytes, marshalErr := json.Marshal(data) - if marshalErr != nil { - return "", marshalErr - } - return string(jsonBytes), nil - } - } - return "", err - } - data, err := client.GetFilteredData(ctx, spotifyURL, false, 0) - if err != nil { - if shouldTrySpotFetchFallback(err) { - fallbackData, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL) - if apiErr == nil { - jsonBytes, marshalErr := json.Marshal(fallbackData) - if marshalErr != nil { - return "", marshalErr - } - return string(jsonBytes), nil - } - } - return "", err - } - - jsonBytes, err := json.Marshal(data) - if err != nil { - return "", err - } - - return string(jsonBytes), nil -} - -func SearchSpotify(query string, limit int) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - - client, err := NewSpotifyMetadataClient() - if err != nil { - return "", err - } - results, err := client.SearchTracks(ctx, query, limit) - if err != nil { - return "", err - } - - jsonBytes, err := json.Marshal(results) - if err != nil { - return "", err - } - - return string(jsonBytes), nil -} - -func SearchSpotifyAll(query string, trackLimit, artistLimit int) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - - client, err := NewSpotifyMetadataClient() - if err != nil { - return "", err - } - results, err := client.SearchAll(ctx, query, trackLimit, artistLimit) - if err != nil { - return "", err - } - - jsonBytes, err := json.Marshal(results) - if err != nil { - return "", err - } - - return string(jsonBytes), nil -} - -func GetSpotifyRelatedArtists(artistID string, limit int) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - - client, err := NewSpotifyMetadataClient() - if err != nil { - return "", err - } - - normalizedArtistID := strings.TrimSpace(strings.TrimPrefix(artistID, "spotify:")) - if normalizedArtistID == "" { - return "", fmt.Errorf("invalid Spotify artist ID") - } - - artists, err := client.GetRelatedArtists(ctx, normalizedArtistID, limit) - if err != nil { - return "", err - } - - resp := map[string]interface{}{ - "artists": artists, - } - jsonBytes, err := json.Marshal(resp) - if err != nil { - return "", err - } - return string(jsonBytes), nil -} - func CheckAvailability(spotifyID, isrc string) (string, error) { client := NewSongLinkClient() availability, err := client.CheckTrackAvailability(spotifyID, isrc) @@ -1439,28 +1319,6 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - var spotifyErr error - - client, err := NewSpotifyMetadataClient() - if err != nil { - LogWarn("Spotify", "Credentials not configured, falling back to Deezer") - spotifyErr = err - } else { - data, err := client.GetFilteredData(ctx, spotifyURL, false, 0) - if err == nil { - jsonBytes, err := json.Marshal(data) - if err != nil { - return "", err - } - return string(jsonBytes), nil - } - - spotifyErr = err - if !shouldTrySpotFetchFallback(err) { - return "", err - } - } - spotFetchData, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL) if apiErr == nil { GoLog("[Fallback] Spotify metadata fetched via SpotFetch API\n") @@ -1474,9 +1332,6 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { parsed, parseErr := parseSpotifyURI(spotifyURL) if parseErr != nil { - if spotifyErr != nil { - return "", fmt.Errorf("spotify failed (%v), SpotFetch fallback failed (%v), and URL parsing failed: %w", spotifyErr, apiErr, parseErr) - } return "", fmt.Errorf("SpotFetch fallback failed (%v) and URL parsing failed: %w", apiErr, parseErr) } @@ -1487,15 +1342,9 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { } if parsed.Type == "artist" { - if spotifyErr != nil { - return "", fmt.Errorf("spotify metadata unavailable (%v) and SpotFetch fallback failed (%v). Artist pages require Spotify/SpotFetch API", spotifyErr, apiErr) - } - return "", fmt.Errorf("SpotFetch fallback failed (%v). Artist pages require Spotify/SpotFetch API", apiErr) + return "", fmt.Errorf("SpotFetch fallback failed (%v). Artist pages now require SpotFetch or a metadata extension such as spotify-web", apiErr) } - if spotifyErr != nil { - return "", fmt.Errorf("spotify metadata unavailable (%v), SpotFetch fallback failed (%v), and Deezer conversion is unavailable for playlists", spotifyErr, apiErr) - } return "", fmt.Errorf("SpotFetch fallback failed (%v), and Deezer conversion is unavailable for playlists", apiErr) } @@ -1826,8 +1675,8 @@ func ReEnrichFile(requestJSON string) (string, error) { GoLog("[ReEnrich] Starting re-enrichment for: %s\n", req.FilePath) - // When search_online is true, search for metadata from internet - // Priority: 1) Deezer (reliable, no credentials) 2) Extension providers (spotify-web etc) 3) Spotify built-in API (last resort, deprecated) + // When search_online is true, search for metadata from internet. + // Priority: 1) Deezer (reliable, no credentials) 2) Extension providers (spotify-web etc) if req.SearchOnline && req.TrackName != "" && req.ArtistName != "" { GoLog("[ReEnrich] Searching online metadata for: %s - %s\n", req.TrackName, req.ArtistName) searchQuery := req.TrackName + " " + req.ArtistName @@ -1901,37 +1750,6 @@ func ReEnrichFile(requestJSON string) (string, error) { } } - // 3) Try Spotify built-in API as last resort (will be deprecated) - if !found { - GoLog("[ReEnrich] Trying Spotify API (fallback)...\n") - spotifyClient, spotifyErr := NewSpotifyMetadataClient() - if spotifyErr == nil { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - results, err := spotifyClient.SearchTracks(ctx, searchQuery, 5) - cancel() - if err == nil && len(results.Tracks) > 0 { - track := results.Tracks[0] - GoLog("[ReEnrich] Spotify match: %s - %s (album: %s)\n", track.Name, track.Artists, track.AlbumName) - req.SpotifyID = track.SpotifyID - req.AlbumName = track.AlbumName - req.AlbumArtist = track.AlbumArtist - req.TrackNumber = track.TrackNumber - req.DiscNumber = track.DiscNumber - req.ReleaseDate = track.ReleaseDate - req.ISRC = track.ISRC - if track.Images != "" { - req.CoverURL = track.Images - } - req.DurationMs = int64(track.DurationMS) - found = true - } else if err != nil { - GoLog("[ReEnrich] Spotify search failed: %v\n", err) - } - } else { - GoLog("[ReEnrich] Spotify client unavailable: %v\n", spotifyErr) - } - } - // Try to get extended metadata (genre, label) from Deezer if not already set if found && req.ISRC != "" && (req.Genre == "" || req.Label == "") { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index a1a419b5..89cf02a2 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -644,8 +644,26 @@ func GetProviderPriority() []string { func SetMetadataProviderPriority(providerIDs []string) { metadataProviderPriorityMu.Lock() defer metadataProviderPriorityMu.Unlock() - metadataProviderPriority = providerIDs - GoLog("[Extension] Metadata provider priority set: %v\n", providerIDs) + + sanitized := make([]string, 0, len(providerIDs)+1) + seen := map[string]struct{}{} + for _, providerID := range providerIDs { + providerID = strings.TrimSpace(providerID) + if providerID == "" || providerID == "spotify" { + continue + } + if _, exists := seen[providerID]; exists { + continue + } + seen[providerID] = struct{}{} + sanitized = append(sanitized, providerID) + } + if _, exists := seen["deezer"]; !exists { + sanitized = append([]string{"deezer"}, sanitized...) + } + + metadataProviderPriority = sanitized + GoLog("[Extension] Metadata provider priority set: %v\n", sanitized) } func GetMetadataProviderPriority() []string { @@ -653,7 +671,7 @@ func GetMetadataProviderPriority() []string { defer metadataProviderPriorityMu.RUnlock() if len(metadataProviderPriority) == 0 { - return []string{"deezer", "spotify"} + return []string{"deezer"} } result := make([]string, len(metadataProviderPriority)) diff --git a/go_backend/spotify.go b/go_backend/spotify.go index 9728a72f..570c4e24 100644 --- a/go_backend/spotify.go +++ b/go_backend/spotify.go @@ -9,7 +9,6 @@ import ( "math/rand" "net/http" "net/url" - "os" "strings" "sync" "time" @@ -64,45 +63,20 @@ var ( credentialsMu sync.RWMutex ) -var ErrNoSpotifyCredentials = errors.New("Spotify credentials not configured. Please set your own Client ID and Secret in Settings, or use Deezer as metadata source (free, no credentials required)") +var ErrNoSpotifyCredentials = errors.New("built-in Spotify API metadata provider has been removed; use Deezer or the spotify-web extension instead") func SetSpotifyCredentials(clientID, clientSecret string) { credentialsMu.Lock() defer credentialsMu.Unlock() - customClientID = clientID - customClientSecret = clientSecret + customClientID = "" + customClientSecret = "" } func HasSpotifyCredentials() bool { - credentialsMu.RLock() - defer credentialsMu.RUnlock() - - if customClientID != "" && customClientSecret != "" { - return true - } - - if os.Getenv("SPOTIFY_CLIENT_ID") != "" && os.Getenv("SPOTIFY_CLIENT_SECRET") != "" { - return true - } - return false } func getCredentials() (string, string, error) { - credentialsMu.RLock() - defer credentialsMu.RUnlock() - - if customClientID != "" && customClientSecret != "" { - return customClientID, customClientSecret, nil - } - - clientID := os.Getenv("SPOTIFY_CLIENT_ID") - clientSecret := os.Getenv("SPOTIFY_CLIENT_SECRET") - - if clientID != "" && clientSecret != "" { - return clientID, clientSecret, nil - } - return "", "", ErrNoSpotifyCredentials } diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 8501a4e3..67e3e9ac 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -104,7 +104,7 @@ class AppSettings { this.askQualityBeforeDownload = true, this.spotifyClientId = '', this.spotifyClientSecret = '', - this.useCustomSpotifyCredentials = true, + this.useCustomSpotifyCredentials = false, this.metadataSource = 'deezer', this.enableLogging = false, this.useExtensionProviders = true, @@ -149,7 +149,7 @@ class AppSettings { String? downloadDirectory, String? storageMode, String? downloadTreeUri, - bool? autoFallback, + bool? autoFallback, bool? embedMetadata, bool? embedLyrics, bool? maxQualityCover, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index cc47ca8e..c3eecb50 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -33,7 +33,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( spotifyClientId: json['spotifyClientId'] as String? ?? '', spotifyClientSecret: json['spotifyClientSecret'] as String? ?? '', useCustomSpotifyCredentials: - json['useCustomSpotifyCredentials'] as bool? ?? true, + json['useCustomSpotifyCredentials'] as bool? ?? false, metadataSource: json['metadataSource'] as String? ?? 'deezer', enableLogging: json['enableLogging'] as bool? ?? false, useExtensionProviders: json['useExtensionProviders'] as bool? ?? true, diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index f53aca0c..58d14ee5 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -823,11 +823,19 @@ class ExtensionNotifier extends Notifier { List priority; if (savedJson != null) { final saved = jsonDecode(savedJson) as List; - priority = saved.map((e) => e as String).toList(); + priority = _sanitizeMetadataProviderPriority( + saved.map((e) => e as String).toList(), + ); _log.d('Loaded metadata provider priority from prefs: $priority'); + await prefs.setString( + _metadataProviderPriorityKey, + jsonEncode(priority), + ); await PlatformBridge.setMetadataProviderPriority(priority); } else { - priority = await PlatformBridge.getMetadataProviderPriority(); + priority = _sanitizeMetadataProviderPriority( + await PlatformBridge.getMetadataProviderPriority(), + ); _log.d('Using default metadata provider priority: $priority'); } @@ -840,11 +848,15 @@ class ExtensionNotifier extends Notifier { Future setMetadataProviderPriority(List priority) async { try { final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_metadataProviderPriorityKey, jsonEncode(priority)); + final sanitized = _sanitizeMetadataProviderPriority(priority); + await prefs.setString( + _metadataProviderPriorityKey, + jsonEncode(sanitized), + ); - await PlatformBridge.setMetadataProviderPriority(priority); - state = state.copyWith(metadataProviderPriority: priority); - _log.d('Saved metadata provider priority: $priority'); + await PlatformBridge.setMetadataProviderPriority(sanitized); + state = state.copyWith(metadataProviderPriority: sanitized); + _log.d('Saved metadata provider priority: $sanitized'); } catch (e) { _log.e('Failed to set metadata provider priority: $e'); state = state.copyWith(error: e.toString()); @@ -880,7 +892,7 @@ class ExtensionNotifier extends Notifier { } List getAllMetadataProviders() { - final providers = ['deezer', 'spotify']; + final providers = ['deezer']; for (final ext in state.extensions) { if (ext.enabled && ext.hasMetadataProvider) { providers.add(ext.id); @@ -889,6 +901,23 @@ class ExtensionNotifier extends Notifier { return providers; } + List _sanitizeMetadataProviderPriority(List input) { + final allowed = getAllMetadataProviders().toSet(); + final result = []; + + for (final provider in input) { + if (allowed.contains(provider) && !result.contains(provider)) { + result.add(provider); + } + } + + if (!result.contains('deezer')) { + result.insert(0, 'deezer'); + } + + return result; + } + List get searchProviders { return state.extensions .where((ext) => ext.enabled && ext.hasCustomSearch) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index a7858319..ed44fea4 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -9,7 +9,7 @@ import 'package:spotiflac_android/utils/logger.dart'; const _settingsKey = 'app_settings'; const _migrationVersionKey = 'settings_migration_version'; -const _currentMigrationVersion = 4; +const _currentMigrationVersion = 5; const _spotifyClientSecretKey = 'spotify_client_secret'; final _log = AppLogger('SettingsProvider'); @@ -41,9 +41,7 @@ class SettingsNotifier extends Notifier { await _normalizeSongLinkRegionIfNeeded(); } - await _loadSpotifyClientSecret(prefs); - - _applySpotifyCredentials(); + await _retireBuiltInSpotifyProvider(); LogBuffer.loggingEnabled = state.enableLogging; @@ -105,6 +103,17 @@ class SettingsNotifier extends Notifier { } state = state.copyWith(lyricsProviders: updatedProviders); } + if (state.metadataSource != 'deezer' || + state.spotifyClientId.isNotEmpty || + state.spotifyClientSecret.isNotEmpty || + state.useCustomSpotifyCredentials) { + state = state.copyWith( + metadataSource: 'deezer', + spotifyClientId: '', + spotifyClientSecret: '', + useCustomSpotifyCredentials: false, + ); + } state = state.copyWith(lastSeenVersion: AppInfo.version); await prefs.setInt(_migrationVersionKey, _currentMigrationVersion); await _saveSettings(); @@ -193,49 +202,28 @@ class SettingsNotifier extends Notifier { await _saveSettings(); } - Future _loadSpotifyClientSecret(SharedPreferences prefs) async { + Future _retireBuiltInSpotifyProvider() async { final storedSecret = await _secureStorage.read( key: _spotifyClientSecretKey, ); - final prefsSecret = state.spotifyClientSecret; - - if ((storedSecret == null || storedSecret.isEmpty) && - prefsSecret.isNotEmpty) { - await _secureStorage.write( - key: _spotifyClientSecretKey, - value: prefsSecret, - ); - } - - final effectiveSecret = (storedSecret != null && storedSecret.isNotEmpty) - ? storedSecret - : (prefsSecret.isNotEmpty ? prefsSecret : ''); - - if (effectiveSecret != state.spotifyClientSecret) { - state = state.copyWith(spotifyClientSecret: effectiveSecret); - } - - if (prefsSecret.isNotEmpty) { - await _saveSettings(); - } - } - - Future _storeSpotifyClientSecret(String secret) async { - if (secret.isEmpty) { + if (storedSecret != null && storedSecret.isNotEmpty) { await _secureStorage.delete(key: _spotifyClientSecretKey); - } else { - await _secureStorage.write(key: _spotifyClientSecretKey, value: secret); } - } - Future _applySpotifyCredentials() async { - if (state.spotifyClientId.isNotEmpty && - state.spotifyClientSecret.isNotEmpty) { - await PlatformBridge.setSpotifyCredentials( - state.spotifyClientId, - state.spotifyClientSecret, - ); + if (state.metadataSource == 'deezer' && + state.spotifyClientId.isEmpty && + state.spotifyClientSecret.isEmpty && + !state.useCustomSpotifyCredentials) { + return; } + + state = state.copyWith( + metadataSource: 'deezer', + spotifyClientId: '', + spotifyClientSecret: '', + useCustomSpotifyCredentials: false, + ); + await _saveSettings(); } void setDefaultService(String service) { @@ -396,45 +384,9 @@ class SettingsNotifier extends Notifier { _saveSettings(); } - void setSpotifyClientId(String clientId) { - state = state.copyWith(spotifyClientId: clientId); - _saveSettings(); - } - - Future setSpotifyClientSecret(String clientSecret) async { - state = state.copyWith(spotifyClientSecret: clientSecret); - await _storeSpotifyClientSecret(clientSecret); - _saveSettings(); - } - - Future setSpotifyCredentials( - String clientId, - String clientSecret, - ) async { - state = state.copyWith( - spotifyClientId: clientId, - spotifyClientSecret: clientSecret, - ); - await _storeSpotifyClientSecret(clientSecret); - _saveSettings(); - _applySpotifyCredentials(); - } - - Future clearSpotifyCredentials() async { - state = state.copyWith(spotifyClientId: '', spotifyClientSecret: ''); - await _storeSpotifyClientSecret(''); - _saveSettings(); - _applySpotifyCredentials(); - } - - void setUseCustomSpotifyCredentials(bool enabled) { - state = state.copyWith(useCustomSpotifyCredentials: enabled); - _saveSettings(); - _applySpotifyCredentials(); - } - void setMetadataSource(String source) { - state = state.copyWith(metadataSource: source); + final normalized = source == 'deezer' ? 'deezer' : 'deezer'; + state = state.copyWith(metadataSource: normalized); _saveSettings(); } diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 8ba3f33b..5adae7d0 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -458,7 +458,8 @@ class TrackNotifier extends Notifier { } // If URL doesn't match any known service, it's unrecognized - final isSpotifyUrl = url.contains('open.spotify.com') || + final isSpotifyUrl = + url.contains('open.spotify.com') || url.contains('spotify.link') || url.startsWith('spotify:'); if (!isSpotifyUrl) { @@ -546,11 +547,7 @@ class TrackNotifier extends Notifier { } } - Future search( - String query, { - String? metadataSource, - String? filterOverride, - }) async { + Future search(String query, {String? filterOverride}) async { final requestId = ++_currentRequestId; // Preserve selected filter during loading @@ -576,7 +573,7 @@ class TrackNotifier extends Notifier { searchProvider != null && searchProvider.isNotEmpty; - final source = metadataSource ?? 'deezer'; + const source = 'deezer'; _log.i( 'Search started: source=$source, query="$query", useExtensions=$useExtensions, filter=$currentFilter', @@ -602,32 +599,20 @@ class TrackNotifier extends Notifier { } } } catch (e) { - _log.w('Extension search failed, falling back to built-in: $e'); + _log.w('Extension search failed, falling back to Deezer: $e'); } } - if (source == 'deezer') { - _log.d('Calling Deezer search API...'); - 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: 2, - ); - _log.i( - 'Spotify returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists', - ); - } + _log.d('Calling Deezer search API...'); + 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', + ); if (!_isRequestValid(requestId)) { _log.w('Search request cancelled (requestId=$requestId)'); diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 65e8fcec..5663caa3 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -549,11 +549,7 @@ class _HomeTabState extends ConsumerState } await ref .read(trackProvider.notifier) - .search( - query, - metadataSource: settings.metadataSource, - filterOverride: selectedFilter, - ); + .search(query, filterOverride: selectedFilter); } ref.read(settingsProvider.notifier).setHasSearchedBefore(); } @@ -587,26 +583,24 @@ class _HomeTabState extends ConsumerState if (trackState.error != null && mounted) { final l10n = context.l10n; final errorMsg = trackState.error!; - final isRateLimit = errorMsg.contains('429') || + final isRateLimit = + errorMsg.contains('429') || errorMsg.toLowerCase().contains('rate limit') || errorMsg.toLowerCase().contains('too many requests'); final displayMessage = errorMsg == 'url_not_recognized' ? l10n.errorUrlNotRecognizedMessage : isRateLimit - ? l10n.errorRateLimitedMessage - : l10n.errorUrlFetchFailed; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(displayMessage)), - ); + ? l10n.errorRateLimitedMessage + : l10n.errorUrlFetchFailed; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(displayMessage))); ref.read(trackProvider.notifier).clear(); } else { _navigateToDetailIfNeeded(); } } else { - final settings = ref.read(settingsProvider); - await ref - .read(trackProvider.notifier) - .search(url, metadataSource: settings.metadataSource); + await ref.read(trackProvider.notifier).search(url); } ref.read(settingsProvider.notifier).setHasSearchedBefore(); } @@ -2315,10 +2309,7 @@ class _HomeTabState extends ConsumerState const SizedBox(height: 4), Text( l10n.errorUrlNotRecognizedMessage, - style: TextStyle( - color: colorScheme.error, - fontSize: 12, - ), + style: TextStyle(color: colorScheme.error, fontSize: 12), ), ], ), @@ -2971,9 +2962,6 @@ class _SearchProviderDropdown extends ConsumerWidget { final currentProvider = ref.watch( settingsProvider.select((s) => s.searchProvider), ); - final metadataSource = ref.watch( - settingsProvider.select((s) => s.metadataSource), - ); final extensions = ref.watch(extensionProvider.select((s) => s.extensions)); final colorScheme = Theme.of(context).colorScheme; @@ -3051,7 +3039,7 @@ class _SearchProviderDropdown extends ConsumerWidget { const SizedBox(width: 12), Expanded( child: Text( - metadataSource == 'spotify' ? 'Spotify' : 'Deezer', + 'Deezer', style: TextStyle( fontWeight: currentProvider == null || currentProvider.isEmpty diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 4523fbee..09b2f656 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -27,10 +27,7 @@ class _SearchScreenState extends ConsumerState { _searchController = TextEditingController(text: widget.query); if (widget.query.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { - final settings = ref.read(settingsProvider); - ref - .read(trackProvider.notifier) - .search(widget.query, metadataSource: settings.metadataSource); + ref.read(trackProvider.notifier).search(widget.query); }); } } @@ -44,10 +41,7 @@ class _SearchScreenState extends ConsumerState { void _search() { final query = _searchController.text.trim(); if (query.isNotEmpty) { - final settings = ref.read(settingsProvider); - ref - .read(trackProvider.notifier) - .search(query, metadataSource: settings.metadataSource); + ref.read(trackProvider.notifier).search(query); } } diff --git a/lib/screens/settings/metadata_provider_priority_page.dart b/lib/screens/settings/metadata_provider_priority_page.dart index 7631e27d..3c614784 100644 --- a/lib/screens/settings/metadata_provider_priority_page.dart +++ b/lib/screens/settings/metadata_provider_priority_page.dart @@ -228,13 +228,6 @@ class _MetadataProviderItem extends StatelessWidget { description: context.l10n.metadataNoRateLimits, isBuiltIn: true, ); - case 'spotify': - return _MetadataProviderInfo( - name: 'Spotify', - icon: Icons.music_note, - description: context.l10n.metadataMayRateLimit, - isBuiltIn: true, - ); default: return _MetadataProviderInfo( name: provider, diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index d57558fa..4a7618d0 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; -import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; @@ -73,68 +72,10 @@ class OptionsSettingsPage extends ConsumerWidget { child: SettingsGroup( children: [ _MetadataSourceSelector( - currentSource: settings.metadataSource, onChanged: (v) => ref .read(settingsProvider.notifier) .setMetadataSource(v), ), - if (settings.metadataSource == 'spotify') ...[ - if (settings.spotifyClientId.isEmpty) - Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Card( - color: Theme.of(context).colorScheme.errorContainer, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Icon( - Icons.warning_amber_rounded, - color: Theme.of( - context, - ).colorScheme.onErrorContainer, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - context.l10n.optionsSpotifyWarning, - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onErrorContainer, - fontSize: 12, - ), - ), - ), - ], - ), - ), - ), - ), - SettingsItem( - icon: Icons.key, - title: context.l10n.optionsSpotifyCredentials, - subtitle: settings.spotifyClientId.isNotEmpty - ? context.l10n.optionsSpotifyCredentialsConfigured( - settings.spotifyClientId.length > 8 - ? settings.spotifyClientId.substring(0, 8) - : settings.spotifyClientId, - ) - : context.l10n.optionsSpotifyCredentialsRequired, - onTap: () => - _showSpotifyCredentialsDialog(context, ref, settings), - trailing: Icon( - settings.spotifyClientId.isNotEmpty - ? Icons.check_circle - : Icons.error_outline, - color: settings.spotifyClientId.isNotEmpty - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.error, - size: 20, - ), - showDivider: false, - ), - ], ], ), ), @@ -372,214 +313,6 @@ class OptionsSettingsPage extends ConsumerWidget { } } } - - void _showSpotifyCredentialsDialog( - BuildContext context, - WidgetRef ref, - AppSettings settings, - ) { - final clientIdController = TextEditingController( - text: settings.spotifyClientId, - ); - final clientSecretController = TextEditingController( - text: settings.spotifyClientSecret, - ); - final colorScheme = Theme.of(context).colorScheme; - - showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - backgroundColor: colorScheme.surface, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), - ), - builder: (context) => Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: SingleChildScrollView( - child: SafeArea( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Container( - width: 32, - height: 4, - margin: const EdgeInsets.only(bottom: 24), - decoration: BoxDecoration( - color: colorScheme.outlineVariant, - borderRadius: BorderRadius.circular(2), - ), - ), - ), - Text( - context.l10n.credentialsTitle, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - context.l10n.credentialsDescription, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - - TextField( - controller: clientIdController, - decoration: InputDecoration( - labelText: context.l10n.credentialsClientId, - hintText: context.l10n.credentialsClientIdHint, - filled: true, - fillColor: colorScheme.surfaceContainerHighest.withValues( - alpha: 0.3, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.primary, - width: 2, - ), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, - ), - prefixIcon: const Icon(Icons.person_outline), - ), - ), - const SizedBox(height: 16), - - TextField( - controller: clientSecretController, - obscureText: true, - decoration: InputDecoration( - labelText: context.l10n.credentialsClientSecret, - hintText: context.l10n.credentialsClientSecretHint, - filled: true, - fillColor: colorScheme.surfaceContainerHighest.withValues( - alpha: 0.3, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.primary, - width: 2, - ), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, - ), - prefixIcon: const Icon(Icons.lock_outline), - ), - ), - - const SizedBox(height: 32), - - FilledButton( - onPressed: () { - final clientId = clientIdController.text.trim(); - final clientSecret = clientSecretController.text.trim(); - - if (clientId.isNotEmpty && clientSecret.isNotEmpty) { - ref - .read(settingsProvider.notifier) - .setSpotifyCredentials(clientId, clientSecret); - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCredentialsSaved, - ), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.snackbarFillAllFields), - ), - ); - } - }, - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: Text( - context.l10n.actionSaveCredentials, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - - if (settings.spotifyClientId.isNotEmpty) ...[ - const SizedBox(height: 12), - TextButton( - onPressed: () { - ref - .read(settingsProvider.notifier) - .clearSpotifyCredentials(); - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCredentialsCleared, - ), - ), - ); - }, - style: TextButton.styleFrom( - foregroundColor: colorScheme.error, - padding: const EdgeInsets.symmetric(vertical: 16), - ), - child: Text(context.l10n.actionRemoveCredentials), - ), - ], - - const SizedBox(height: 16), - ], - ), - ), - ), - ), - ), - ); - } } class _ConcurrentDownloadsItem extends StatelessWidget { @@ -875,12 +608,8 @@ class _ChannelChip extends StatelessWidget { } class _MetadataSourceSelector extends ConsumerWidget { - final String currentSource; final ValueChanged onChanged; - const _MetadataSourceSelector({ - required this.currentSource, - required this.onChanged, - }); + const _MetadataSourceSelector({required this.onChanged}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -930,7 +659,7 @@ class _MetadataSourceSelector extends ConsumerWidget { _SourceChip( icon: Icons.graphic_eq, label: 'Deezer', - isSelected: currentSource == 'deezer' && !hasExtensionSearch, + isSelected: !hasExtensionSearch, onTap: () { if (hasExtensionSearch) { ref.read(settingsProvider.notifier).setSearchProvider(null); @@ -938,18 +667,6 @@ class _MetadataSourceSelector extends ConsumerWidget { onChanged('deezer'); }, ), - const SizedBox(width: 12), - _SourceChip( - icon: Icons.music_note, - label: 'Spotify', - isSelected: currentSource == 'spotify' && !hasExtensionSearch, - onTap: () { - if (hasExtensionSearch) { - ref.read(settingsProvider.notifier).setSearchProvider(null); - } - onChanged('spotify'); - }, - ), ], ), if (hasExtensionSearch) ...[ @@ -964,7 +681,7 @@ class _MetadataSourceSelector extends ConsumerWidget { const SizedBox(width: 8), Expanded( child: Text( - context.l10n.optionsSwitchBack, + 'Tap Deezer to switch back from extension', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -973,27 +690,6 @@ class _MetadataSourceSelector extends ConsumerWidget { ], ), ], - if (currentSource == 'spotify' && !hasExtensionSearch) ...[ - const SizedBox(height: 12), - Row( - children: [ - Icon( - Icons.warning_amber_rounded, - size: 16, - color: colorScheme.error, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - context.l10n.optionsSpotifyDeprecationWarning, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: colorScheme.error), - ), - ), - ], - ), - ], ], ), ); diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index bf20d621..c41207b9 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -20,51 +20,6 @@ class PlatformBridge { return jsonDecode(result as String) as Map; } - static Future> getSpotifyMetadata(String url) async { - _log.d('getSpotifyMetadata: $url'); - final result = await _channel.invokeMethod('getSpotifyMetadata', { - 'url': url, - }); - return jsonDecode(result as String) as Map; - } - - static Future> searchSpotify( - String query, { - int limit = 10, - }) async { - _log.d('searchSpotify: "$query" (limit: $limit)'); - final result = await _channel.invokeMethod('searchSpotify', { - 'query': query, - 'limit': limit, - }); - return jsonDecode(result as String) as Map; - } - - static Future> searchSpotifyAll( - String query, { - int trackLimit = 15, - int artistLimit = 3, - }) async { - _log.d('searchSpotifyAll: "$query"'); - final result = await _channel.invokeMethod('searchSpotifyAll', { - 'query': query, - 'track_limit': trackLimit, - 'artist_limit': artistLimit, - }); - return jsonDecode(result as String) as Map; - } - - static Future> getSpotifyRelatedArtists( - String artistId, { - int limit = 12, - }) async { - final result = await _channel.invokeMethod('getSpotifyRelatedArtists', { - 'artist_id': artistId, - 'limit': limit, - }); - return jsonDecode(result as String) as Map; - } - static Future> checkAvailability( String spotifyId, String isrc, @@ -517,21 +472,6 @@ class PlatformBridge { return result as bool; } - static Future setSpotifyCredentials( - String clientId, - String clientSecret, - ) async { - await _channel.invokeMethod('setSpotifyCredentials', { - 'client_id': clientId, - 'client_secret': clientSecret, - }); - } - - static Future hasSpotifyCredentials() async { - final result = await _channel.invokeMethod('hasSpotifyCredentials'); - return result as bool; - } - static Future preWarmTrackCache( List> tracks, ) async { @@ -1313,5 +1253,4 @@ class PlatformBridge { }); return jsonDecode(result as String) as Map; } - }