mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
421 lines
14 KiB
Dart
421 lines
14 KiB
Dart
// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
|
part of 'extension_provider.dart';
|
|
|
|
/// Download/metadata/search provider priority: persistence, sanitizing, and
|
|
/// reconciliation when extensions are installed, removed, or toggled.
|
|
extension ExtensionNotifierProviderPriority on ExtensionNotifier {
|
|
Future<void> _reconcileDownloadProviderPriority() async {
|
|
if (state.providerPriority.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
final sanitized = _sanitizeDownloadProviderPriority(state.providerPriority);
|
|
if (_stringListEquals(sanitized, state.providerPriority)) {
|
|
return;
|
|
}
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_providerPriorityKey, jsonEncode(sanitized));
|
|
await PlatformBridge.setProviderPriority(sanitized);
|
|
state = state.copyWith(providerPriority: sanitized);
|
|
_log.d('Reconciled provider priority after extension update: $sanitized');
|
|
}
|
|
|
|
Future<void> _reconcileMetadataProviderPriority() async {
|
|
if (state.metadataProviderPriority.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
final replaced = _replaceRetiredBuiltInMetadataProviders(
|
|
state.metadataProviderPriority,
|
|
);
|
|
final sanitized = _sanitizeMetadataProviderPriority(replaced);
|
|
if (_stringListEquals(sanitized, state.metadataProviderPriority)) {
|
|
return;
|
|
}
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_metadataProviderPriorityKey, jsonEncode(sanitized));
|
|
await PlatformBridge.setMetadataProviderPriority(sanitized);
|
|
state = state.copyWith(metadataProviderPriority: sanitized);
|
|
_log.d(
|
|
'Reconciled metadata provider priority after extension update: $sanitized',
|
|
);
|
|
}
|
|
|
|
String? _firstEnabledExtensionDownloadProviderId() {
|
|
return state.extensions
|
|
.where((ext) => ext.enabled && ext.hasDownloadProvider)
|
|
.map((ext) => ext.id)
|
|
.firstOrNull;
|
|
}
|
|
|
|
String? _firstEnabledSearchProviderId() {
|
|
return defaultSearchExtension(state.extensions)?.id;
|
|
}
|
|
|
|
String? _replacedBuiltInProviderFor(
|
|
String providerId,
|
|
bool Function(Extension ext) hasCapability,
|
|
) {
|
|
final normalized = providerId.trim().toLowerCase();
|
|
if (normalized.isEmpty) return null;
|
|
|
|
return state.extensions
|
|
.where(
|
|
(ext) =>
|
|
ext.enabled &&
|
|
hasCapability(ext) &&
|
|
ext.replacesBuiltInProviders.contains(normalized),
|
|
)
|
|
.map((ext) => ext.id)
|
|
.firstOrNull;
|
|
}
|
|
|
|
String? replacedBuiltInDownloadProviderFor(String providerId) =>
|
|
_replacedBuiltInProviderFor(providerId, (ext) => ext.hasDownloadProvider);
|
|
|
|
String? replacedBuiltInSearchProviderFor(String providerId) =>
|
|
_replacedBuiltInProviderFor(providerId, (ext) => ext.hasCustomSearch);
|
|
|
|
String? replacedBuiltInMetadataProviderFor(String providerId) =>
|
|
_replacedBuiltInProviderFor(providerId, (ext) => ext.hasMetadataProvider);
|
|
|
|
bool downloadProviderReplacesLegacyProvider(
|
|
String providerId,
|
|
String legacyProviderId,
|
|
) {
|
|
final normalizedProvider = providerId.trim().toLowerCase();
|
|
final normalizedLegacy = legacyProviderId.trim().toLowerCase();
|
|
if (normalizedProvider.isEmpty || normalizedLegacy.isEmpty) return false;
|
|
if (normalizedProvider == normalizedLegacy) return true;
|
|
|
|
final extension = state.extensions
|
|
.where((ext) => ext.enabled && ext.hasDownloadProvider)
|
|
.where((ext) => ext.id.toLowerCase() == normalizedProvider)
|
|
.firstOrNull;
|
|
return extension?.replacesBuiltInProviders.contains(normalizedLegacy) ??
|
|
false;
|
|
}
|
|
|
|
Future<void> _reconcileDefaultDownloadService() async {
|
|
final settings = ref.read(settingsProvider);
|
|
final preferredExtensionId = _firstEnabledExtensionDownloadProviderId();
|
|
final currentService = settings.defaultService.trim();
|
|
|
|
if (currentService.isEmpty) {
|
|
if (preferredExtensionId != null) {
|
|
ref
|
|
.read(settingsProvider.notifier)
|
|
.setDefaultService(preferredExtensionId);
|
|
_log.d(
|
|
'Adopted first enabled download extension as default service: $preferredExtensionId',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
final replacementExtensionId = replacedBuiltInDownloadProviderFor(
|
|
currentService,
|
|
);
|
|
if (replacementExtensionId != null) {
|
|
ref
|
|
.read(settingsProvider.notifier)
|
|
.setDefaultService(replacementExtensionId);
|
|
_log.d(
|
|
'Migrated retired built-in service $currentService to $replacementExtensionId',
|
|
);
|
|
return;
|
|
}
|
|
|
|
final currentExtension = state.extensions
|
|
.where((ext) => ext.id == currentService)
|
|
.firstOrNull;
|
|
final isMissingOrInvalidExtension =
|
|
currentExtension == null ||
|
|
!currentExtension.enabled ||
|
|
!currentExtension.hasDownloadProvider;
|
|
if (isMissingOrInvalidExtension) {
|
|
final fallbackService = preferredExtensionId ?? '';
|
|
ref.read(settingsProvider.notifier).setDefaultService(fallbackService);
|
|
_log.d(
|
|
fallbackService.isEmpty
|
|
? 'Cleared default service because $currentService is no longer available'
|
|
: 'Reset default service to $fallbackService because $currentService is no longer available',
|
|
);
|
|
}
|
|
}
|
|
|
|
void _reconcileSearchProvider() {
|
|
final settings = ref.read(settingsProvider);
|
|
final currentSearchProvider = settings.searchProvider?.trim() ?? '';
|
|
final preferredSearchProvider = _firstEnabledSearchProviderId() ?? '';
|
|
|
|
if (currentSearchProvider.isEmpty) {
|
|
if (preferredSearchProvider.isNotEmpty) {
|
|
ref
|
|
.read(settingsProvider.notifier)
|
|
.setSearchProvider(preferredSearchProvider);
|
|
_log.d(
|
|
'Adopted first enabled search provider as default: $preferredSearchProvider',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
final replacementExtensionId = replacedBuiltInSearchProviderFor(
|
|
currentSearchProvider,
|
|
);
|
|
if (replacementExtensionId != null) {
|
|
ref
|
|
.read(settingsProvider.notifier)
|
|
.setSearchProvider(replacementExtensionId);
|
|
_log.d(
|
|
'Migrated retired built-in search provider $currentSearchProvider to $replacementExtensionId',
|
|
);
|
|
return;
|
|
}
|
|
|
|
final hasMatchingExtension = state.extensions.any(
|
|
(ext) =>
|
|
ext.enabled && ext.hasCustomSearch && ext.id == currentSearchProvider,
|
|
);
|
|
if (!hasMatchingExtension) {
|
|
ref
|
|
.read(settingsProvider.notifier)
|
|
.setSearchProvider(
|
|
preferredSearchProvider.isNotEmpty ? preferredSearchProvider : null,
|
|
);
|
|
_log.d(
|
|
preferredSearchProvider.isNotEmpty
|
|
? 'Reset stale search provider $currentSearchProvider to $preferredSearchProvider'
|
|
: 'Cleared stale search provider because $currentSearchProvider is no longer available',
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<Map<String, dynamic>> getExtensionSettings(String extensionId) async {
|
|
try {
|
|
return await PlatformBridge.getExtensionSettings(extensionId);
|
|
} catch (e) {
|
|
_log.e('Failed to get extension settings: $e');
|
|
return {};
|
|
}
|
|
}
|
|
|
|
Future<void> setExtensionSettings(
|
|
String extensionId,
|
|
Map<String, dynamic> settings,
|
|
) async {
|
|
try {
|
|
await PlatformBridge.setExtensionSettings(extensionId, settings);
|
|
_log.d('Updated settings for extension: $extensionId');
|
|
} catch (e) {
|
|
_log.e('Failed to set extension settings: $e');
|
|
state = state.copyWith(error: e.toString());
|
|
}
|
|
}
|
|
|
|
/// Shared load path for the download/metadata priority lists: prefs first
|
|
/// (sanitized), falling back to backend defaults, then persist + push the
|
|
/// result back to the backend.
|
|
Future<List<String>> _loadPriorityList({
|
|
required String prefsKey,
|
|
required String label,
|
|
required List<String> Function(List<String>) sanitizeStored,
|
|
required List<String> Function(List<String>) sanitizeBackend,
|
|
required Future<List<String>> Function() fetchBackend,
|
|
required Future<void> Function(List<String>) pushBackend,
|
|
}) async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final savedJson = prefs.getString(prefsKey);
|
|
|
|
List<String> priority;
|
|
if (savedJson != null) {
|
|
final saved = _tryDecodeStringListPreference(savedJson, prefsKey);
|
|
if (saved != null) {
|
|
priority = sanitizeStored(saved);
|
|
_log.d('Loaded $label from prefs: $priority');
|
|
} else {
|
|
await prefs.remove(prefsKey);
|
|
priority = sanitizeBackend(await fetchBackend());
|
|
_log.d('Recovered $label from defaults: $priority');
|
|
}
|
|
} else {
|
|
priority = sanitizeBackend(await fetchBackend());
|
|
_log.d('Using default $label: $priority');
|
|
}
|
|
await prefs.setString(prefsKey, jsonEncode(priority));
|
|
await pushBackend(priority);
|
|
return priority;
|
|
}
|
|
|
|
Future<void> loadProviderPriority() async {
|
|
try {
|
|
final priority = await _loadPriorityList(
|
|
prefsKey: _providerPriorityKey,
|
|
label: 'provider priority',
|
|
sanitizeStored: _sanitizeDownloadProviderPriority,
|
|
sanitizeBackend: _sanitizeDownloadProviderPriority,
|
|
fetchBackend: PlatformBridge.getProviderPriority,
|
|
pushBackend: PlatformBridge.setProviderPriority,
|
|
);
|
|
state = state.copyWith(providerPriority: priority);
|
|
} catch (e) {
|
|
_log.e('Failed to load provider priority: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> setProviderPriority(List<String> priority) async {
|
|
try {
|
|
final sanitized = _sanitizeDownloadProviderPriority(priority);
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_providerPriorityKey, jsonEncode(sanitized));
|
|
|
|
await PlatformBridge.setProviderPriority(sanitized);
|
|
state = state.copyWith(providerPriority: sanitized);
|
|
_log.d('Saved provider priority: $sanitized');
|
|
} catch (e) {
|
|
_log.e('Failed to set provider priority: $e');
|
|
state = state.copyWith(error: e.toString());
|
|
}
|
|
}
|
|
|
|
List<String> _sanitizeDownloadProviderPriority(List<String> input) {
|
|
final allowed = getAllDownloadProviders().toSet();
|
|
final preferredOrder = getAllDownloadProviders();
|
|
final result = <String>[];
|
|
|
|
for (final provider in input) {
|
|
if (allowed.contains(provider) && !result.contains(provider)) {
|
|
result.add(provider);
|
|
}
|
|
}
|
|
|
|
for (final provider in preferredOrder) {
|
|
if (!result.contains(provider)) {
|
|
result.add(provider);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Future<void> loadMetadataProviderPriority() async {
|
|
try {
|
|
final priority = await _loadPriorityList(
|
|
prefsKey: _metadataProviderPriorityKey,
|
|
label: 'metadata provider priority',
|
|
sanitizeStored: (saved) => _sanitizeMetadataProviderPriority(
|
|
_replaceRetiredBuiltInMetadataProviders(saved),
|
|
),
|
|
sanitizeBackend: _sanitizeMetadataProviderPriority,
|
|
fetchBackend: PlatformBridge.getMetadataProviderPriority,
|
|
pushBackend: PlatformBridge.setMetadataProviderPriority,
|
|
);
|
|
state = state.copyWith(metadataProviderPriority: priority);
|
|
} catch (e) {
|
|
_log.e('Failed to load metadata provider priority: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> setMetadataProviderPriority(List<String> priority) async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final sanitized = _sanitizeMetadataProviderPriority(
|
|
_replaceRetiredBuiltInMetadataProviders(priority),
|
|
);
|
|
await prefs.setString(
|
|
_metadataProviderPriorityKey,
|
|
jsonEncode(sanitized),
|
|
);
|
|
|
|
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());
|
|
}
|
|
}
|
|
|
|
Future<void> cleanup() async {
|
|
if (_cleanupInFlight) return;
|
|
_cleanupInFlight = true;
|
|
await _cleanupExtensions(reason: 'manual');
|
|
}
|
|
|
|
List<String> getAllDownloadProviders() {
|
|
return _distinctProviderIds(
|
|
state.extensions
|
|
.where((ext) => ext.enabled && ext.hasDownloadProvider)
|
|
.map((ext) => ext.id),
|
|
);
|
|
}
|
|
|
|
List<String> getAllMetadataProviders() {
|
|
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 _distinctProviderIds([
|
|
...primarySearchMetadataExtensions,
|
|
...otherMetadataExtensions,
|
|
]);
|
|
}
|
|
|
|
List<String> _distinctProviderIds(Iterable<String> ids) {
|
|
final seen = <String>{};
|
|
final result = <String>[];
|
|
for (final id in ids) {
|
|
final normalized = id.trim();
|
|
if (normalized.isNotEmpty && seen.add(normalized)) {
|
|
result.add(normalized);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
List<String> _replaceRetiredBuiltInMetadataProviders(List<String> input) {
|
|
final result = <String>[];
|
|
for (final provider in input) {
|
|
final replacement = replacedBuiltInMetadataProviderFor(provider);
|
|
final resolved = replacement ?? provider;
|
|
if (!result.contains(resolved)) {
|
|
result.add(resolved);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
List<String> _sanitizeMetadataProviderPriority(List<String> input) {
|
|
final allowed = getAllMetadataProviders().toSet();
|
|
final preferredOrder = getAllMetadataProviders();
|
|
final result = <String>[];
|
|
|
|
for (final provider in input) {
|
|
if (allowed.contains(provider) && !result.contains(provider)) {
|
|
result.add(provider);
|
|
}
|
|
}
|
|
|
|
if (result.isEmpty && preferredOrder.isNotEmpty) {
|
|
return List<String>.from(preferredOrder);
|
|
}
|
|
|
|
for (final provider in preferredOrder) {
|
|
if (!result.contains(provider)) {
|
|
result.add(provider);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|