From 8f2ca33e879e7772dc1a5d2f748ed114675964ec Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 18 Apr 2026 23:10:00 +0700 Subject: [PATCH] refactor: remove Qobuz from built-in provider registry, add retired provider detection Empty the builtInProviderRegistry now that all built-in providers are retired. Introduce isRetiredBuiltInDownloadProvider and isRetiredBuiltInMetadataProvider to classify deezer/qobuz/tidal/spotify as retired, replacing ad-hoc string checks. Add Dart-side metadata provider priority reconciliation that replaces retired providers with extensions declaring replacesBuiltInProviders. Remove getQobuzMetadata from native bridges and platform_bridge.dart. Update crowdin.yml with additional locale mappings. --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 8 --- crowdin.yml | 4 ++ go_backend/exports.go | 1 - go_backend/extension_providers.go | 52 ++++++++++------ go_backend/extension_providers_test.go | 45 ++++---------- ios/Runner/AppDelegate.swift | 8 --- lib/providers/extension_provider.dart | 59 ++++++++++++++++++- lib/services/platform_bridge.dart | 16 ----- 8 files changed, 108 insertions(+), 85 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 1cd1fb08..dcb38bf5 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -2889,14 +2889,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - "getQobuzMetadata" -> { - val resourceType = call.argument("resource_type") ?: "" - val resourceId = call.argument("resource_id") ?: "" - val response = withContext(Dispatchers.IO) { - Gobackend.getQobuzMetadata(resourceType, resourceId) - } - result.success(response) - } "getProviderMetadata" -> { val providerId = call.argument("provider_id") ?: "" val resourceType = call.argument("resource_type") ?: "" diff --git a/crowdin.yml b/crowdin.yml index d19e6faf..a3d8a89b 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -6,6 +6,7 @@ files: # Short codes for single-variant languages de: de es: es + es-ES: es_ES fr: fr hi: hi id: id @@ -13,7 +14,10 @@ files: ko: ko nl: nl pt: pt + pt-PT: pt_PT ru: ru + tr: tr + zh: zh # Full codes for Chinese variants zh-CN: zh_CN zh-TW: zh_TW diff --git a/go_backend/exports.go b/go_backend/exports.go index 32270a6c..1fcc22ee 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -2358,7 +2358,6 @@ func ParseProviderURLJSON(url string) (string, error) { parse func(string) (string, string, error) }{ {providerID: "deezer", parse: parseDeezerURL}, - {providerID: "qobuz", parse: parseQobuzURL}, } for _, parser := range parsers { diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index b8bff343..bba2d890 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -108,21 +108,7 @@ type builtInProviderSpec struct { Download func(req DownloadRequest) (DownloadResult, error) `json:"-"` } -var builtInProviderRegistry = []builtInProviderSpec{ - { - ID: "qobuz", - DisplayName: "Qobuz", - SupportsMetadata: true, - SupportsDownload: true, - SupportsSearch: true, - GetMetadata: GetQobuzMetadata, - SearchAll: SearchQobuzAll, - SearchTracks: func(query string, limit int) ([]ExtTrackMetadata, error) { - return NewQobuzDownloader().SearchTracks(query, limit) - }, - Download: downloadWithBuiltInQobuz, - }, -} +var builtInProviderRegistry = []builtInProviderSpec{} func getBuiltInProviderSpecs() []builtInProviderSpec { specs := make([]builtInProviderSpec, len(builtInProviderRegistry)) @@ -1224,7 +1210,7 @@ func sanitizeDownloadProviderPriority(providerIDs []string) []string { } normalizedBuiltIn := strings.ToLower(providerID) - if normalizedBuiltIn == "deezer" { + if isRetiredBuiltInDownloadProvider(normalizedBuiltIn) { continue } if isBuiltInDownloadProvider(normalizedBuiltIn) { @@ -1242,6 +1228,38 @@ func sanitizeDownloadProviderPriority(providerIDs []string) []string { return sanitized } +func isRetiredBuiltInDownloadProvider(providerID string) bool { + normalized := strings.ToLower(strings.TrimSpace(providerID)) + if normalized == "" { + return false + } + if isBuiltInDownloadProvider(normalized) { + return false + } + switch normalized { + case "deezer", "qobuz", "tidal": + return true + default: + return false + } +} + +func isRetiredBuiltInMetadataProvider(providerID string) bool { + normalized := strings.ToLower(strings.TrimSpace(providerID)) + if normalized == "" { + return false + } + if isBuiltInMetadataProvider(normalized) { + return false + } + switch normalized { + case "spotify", "qobuz", "tidal": + return true + default: + return false + } +} + func SetExtensionFallbackProviderIDs(providerIDs []string) { extensionFallbackProviderIDsMu.Lock() defer extensionFallbackProviderIDsMu.Unlock() @@ -1309,7 +1327,7 @@ func SetMetadataProviderPriority(providerIDs []string) { seen := map[string]struct{}{} for _, providerID := range providerIDs { providerID = strings.TrimSpace(providerID) - if providerID == "" || providerID == "spotify" { + if providerID == "" || isRetiredBuiltInMetadataProvider(providerID) { continue } if _, exists := seen[providerID]; exists { diff --git a/go_backend/extension_providers_test.go b/go_backend/extension_providers_test.go index 23ea1621..f4d5b682 100644 --- a/go_backend/extension_providers_test.go +++ b/go_backend/extension_providers_test.go @@ -7,28 +7,22 @@ import ( "testing" ) -func TestSetMetadataProviderPriorityPreservesExplicitProvidersOnly(t *testing.T) { +func TestSetMetadataProviderPriorityStripsRetiredBuiltIns(t *testing.T) { original := GetMetadataProviderPriority() defer SetMetadataProviderPriority(original) SetMetadataProviderPriority([]string{"qobuz"}) got := GetMetadataProviderPriority() - want := []string{"qobuz"} - if len(got) != len(want) { - t.Fatalf("unexpected priority length: got %v want %v", got, want) - } - for i := range want { - if got[i] != want[i] { - t.Fatalf("unexpected priority at %d: got %v want %v", i, got, want) - } + if len(got) != 0 { + t.Fatalf("expected retired built-in qobuz to be stripped, got %v", got) } } -func TestSetExtensionFallbackProviderIDsSkipsBuiltInsAndDuplicates(t *testing.T) { +func TestSetExtensionFallbackProviderIDsDedupesExtensions(t *testing.T) { original := GetExtensionFallbackProviderIDs() defer SetExtensionFallbackProviderIDs(original) - SetExtensionFallbackProviderIDs([]string{"ext-a", "qobuz", "ext-a", " ext-b "}) + SetExtensionFallbackProviderIDs([]string{"ext-a", "ext-a", " ext-b "}) got := GetExtensionFallbackProviderIDs() want := []string{"ext-a", "ext-b"} @@ -51,9 +45,6 @@ func TestIsExtensionFallbackAllowedDefaultsToAllExtensions(t *testing.T) { if !isExtensionFallbackAllowed("custom-ext") { t.Fatal("expected custom extension to be allowed when no fallback allowlist is configured") } - if !isExtensionFallbackAllowed("qobuz") { - t.Fatal("expected built-in provider to remain allowed") - } } func TestIsExtensionFallbackAllowedRespectsAllowlist(t *testing.T) { @@ -80,7 +71,7 @@ func TestSetProviderPriorityRemovesRetiredDeezerDownloader(t *testing.T) { SetProviderPriority([]string{"deezer", "qobuz", "custom-ext"}) got := GetProviderPriority() - want := []string{"qobuz", "custom-ext"} + want := []string{"custom-ext"} if len(got) != len(want) { t.Fatalf("unexpected priority length: got %v want %v", got, want) } @@ -247,7 +238,7 @@ func TestCanEmbedGenreLabelRequiresExistingAbsoluteLocalFile(t *testing.T) { } } -func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) { +func TestSearchTracksWithMetadataProvidersIgnoresRetiredBuiltIns(t *testing.T) { originalPriority := GetMetadataProviderPriority() originalSearch := searchBuiltInMetadataTracksFunc defer func() { @@ -260,16 +251,7 @@ func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) { var calls []string searchBuiltInMetadataTracksFunc = func(providerID, query string, limit int) ([]ExtTrackMetadata, error) { calls = append(calls, providerID) - switch providerID { - case "qobuz": - return []ExtTrackMetadata{ - {ProviderID: "qobuz", SpotifyID: "qobuz:1", ISRC: "AAA111", Name: "First"}, - {ProviderID: "qobuz", SpotifyID: "qobuz:2", ISRC: "AAA111", Name: "Duplicate"}, - {ProviderID: "qobuz", SpotifyID: "qobuz:3", ISRC: "BBB222", Name: "Second"}, - }, nil - default: - return nil, nil - } + return nil, nil } manager := getExtensionManager() @@ -277,13 +259,10 @@ func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) { if err != nil { t.Fatalf("SearchTracksWithMetadataProviders returned error: %v", err) } - if len(tracks) != 2 { - t.Fatalf("unexpected track count: got %d want 2", len(tracks)) + if len(tracks) != 0 { + t.Fatalf("expected no tracks from retired built-in provider, got %+v", tracks) } - if tracks[0].ProviderID != "qobuz" || tracks[1].ProviderID != "qobuz" { - t.Fatalf("unexpected track provider order: %+v", tracks) - } - if len(calls) != 1 || calls[0] != "qobuz" { - t.Fatalf("unexpected provider call order: %v", calls) + if len(calls) != 0 { + t.Fatalf("expected retired built-in provider not to be queried, got %v", calls) } } diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 846a0987..fda8ebdb 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -448,14 +448,6 @@ import Gobackend // Import Go framework if let error = error { throw error } return response - case "getQobuzMetadata": - let args = call.arguments as! [String: Any] - let resourceType = args["resource_type"] as! String - let resourceId = args["resource_id"] as! String - let response = GobackendGetQobuzMetadata(resourceType, resourceId, &error) - if let error = error { throw error } - return response - case "getProviderMetadata": let args = call.arguments as! [String: Any] let providerId = args["provider_id"] as! String diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index 12ef6add..a38126a5 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -758,6 +758,7 @@ class ExtensionNotifier extends Notifier { state = state.copyWith(extensions: extensions); await _reconcileDownloadProviderPriority(); await _reconcileDefaultDownloadService(); + await _reconcileMetadataProviderPriority(); _reconcileSearchProvider(); _log.d('Loaded ${extensions.length} extensions'); @@ -866,6 +867,7 @@ class ExtensionNotifier extends Notifier { state = state.copyWith(extensions: extensions); await _reconcileDownloadProviderPriority(); await _reconcileDefaultDownloadService(); + await _reconcileMetadataProviderPriority(); _reconcileSearchProvider(); if (!enabled && ext != null) { @@ -914,6 +916,28 @@ class ExtensionNotifier extends Notifier { _log.d('Reconciled provider priority after extension update: $sanitized'); } + Future _reconcileMetadataProviderPriority() async { + if (state.metadataProviderPriority.isEmpty) { + return; + } + + final replaced = _replaceRetiredBuiltInMetadataProviders( + state.metadataProviderPriority, + ); + final sanitized = _sanitizeMetadataProviderPriority(replaced); + if (jsonEncode(sanitized) == jsonEncode(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) @@ -951,6 +975,21 @@ class ExtensionNotifier extends Notifier { .firstOrNull; } + String? replacedBuiltInMetadataProviderFor(String providerId) { + final normalized = providerId.trim().toLowerCase(); + if (normalized.isEmpty) return null; + + return state.extensions + .where( + (ext) => + ext.enabled && + ext.hasMetadataProvider && + ext.replacesBuiltInProviders.contains(normalized), + ) + .map((ext) => ext.id) + .firstOrNull; + } + bool downloadProviderMatchesBuiltIn( String providerId, String builtInProviderId, @@ -1204,7 +1243,9 @@ class ExtensionNotifier extends Notifier { if (savedJson != null) { final saved = jsonDecode(savedJson) as List; priority = _sanitizeMetadataProviderPriority( - saved.map((e) => e as String).toList(), + _replaceRetiredBuiltInMetadataProviders( + saved.map((e) => e as String).toList(), + ), ); _log.d('Loaded metadata provider priority from prefs: $priority'); await prefs.setString( @@ -1233,7 +1274,9 @@ class ExtensionNotifier extends Notifier { Future setMetadataProviderPriority(List priority) async { try { final prefs = await SharedPreferences.getInstance(); - final sanitized = _sanitizeMetadataProviderPriority(priority); + final sanitized = _sanitizeMetadataProviderPriority( + _replaceRetiredBuiltInMetadataProviders(priority), + ); await prefs.setString( _metadataProviderPriorityKey, jsonEncode(sanitized), @@ -1294,6 +1337,18 @@ class ExtensionNotifier extends Notifier { ]; } + List _replaceRetiredBuiltInMetadataProviders(List input) { + final result = []; + for (final provider in input) { + final replacement = replacedBuiltInMetadataProviderFor(provider); + final resolved = replacement ?? provider; + if (!result.contains(resolved)) { + result.add(resolved); + } + } + return result; + } + List _sanitizeMetadataProviderPriority(List input) { final allowed = getAllMetadataProviders().toSet(); final preferredOrder = getAllMetadataProviders(); diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index abbbacad..e85f712f 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -524,22 +524,6 @@ class PlatformBridge { return jsonDecode(result as String) as Map; } - static Future> getQobuzMetadata( - String resourceType, - String resourceId, - ) async { - final result = await _channel.invokeMethod('getQobuzMetadata', { - 'resource_type': resourceType, - 'resource_id': resourceId, - }); - if (result == null) { - throw Exception( - 'getQobuzMetadata returned null for $resourceType:$resourceId', - ); - } - return jsonDecode(result as String) as Map; - } - static Future> parseProviderUrl(String url) async { final result = await _channel.invokeMethod('parseProviderUrl', { 'url': url,