refactor: remove built-in Spotify API provider, use Deezer as sole default

- 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
This commit is contained in:
zarzet
2026-03-11 00:58:07 +07:00
parent 76fe8dbc69
commit c2736a61fb
14 changed files with 127 additions and 787 deletions
+11 -23
View File
@@ -549,11 +549,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
}
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<HomeTab>
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<HomeTab>
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
+2 -8
View File
@@ -27,10 +27,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
_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<SearchScreen> {
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);
}
}
@@ -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,
+3 -307
View File
@@ -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<String> 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),
),
),
],
),
],
],
),
);