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.
This commit is contained in:
zarzet
2026-04-18 23:10:00 +07:00
parent 16ce6089fb
commit 8f2ca33e87
8 changed files with 108 additions and 85 deletions
@@ -2889,14 +2889,6 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(response)
}
"getQobuzMetadata" -> {
val resourceType = call.argument<String>("resource_type") ?: ""
val resourceId = call.argument<String>("resource_id") ?: ""
val response = withContext(Dispatchers.IO) {
Gobackend.getQobuzMetadata(resourceType, resourceId)
}
result.success(response)
}
"getProviderMetadata" -> {
val providerId = call.argument<String>("provider_id") ?: ""
val resourceType = call.argument<String>("resource_type") ?: ""
+4
View File
@@ -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
-1
View File
@@ -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 {
+35 -17
View File
@@ -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 {
+12 -33
View File
@@ -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)
}
}
-8
View File
@@ -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
+57 -2
View File
@@ -758,6 +758,7 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
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<ExtensionState> {
state = state.copyWith(extensions: extensions);
await _reconcileDownloadProviderPriority();
await _reconcileDefaultDownloadService();
await _reconcileMetadataProviderPriority();
_reconcileSearchProvider();
if (!enabled && ext != null) {
@@ -914,6 +916,28 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
_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 (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<ExtensionState> {
.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<ExtensionState> {
if (savedJson != null) {
final saved = jsonDecode(savedJson) as List<dynamic>;
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<ExtensionState> {
Future<void> setMetadataProviderPriority(List<String> 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<ExtensionState> {
];
}
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();
-16
View File
@@ -524,22 +524,6 @@ class PlatformBridge {
return jsonDecode(result as String) as Map<String, dynamic>;
}
static Future<Map<String, dynamic>> 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<String, dynamic>;
}
static Future<Map<String, dynamic>> parseProviderUrl(String url) async {
final result = await _channel.invokeMethod('parseProviderUrl', {
'url': url,