mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-17 22:04:47 +02:00
refactor: move deezer search flow to extension
This commit is contained in:
@@ -2734,16 +2734,6 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"searchDeezerAll" -> {
|
||||
val query = call.argument<String>("query") ?: ""
|
||||
val trackLimit = call.argument<Int>("track_limit") ?: 15
|
||||
val artistLimit = call.argument<Int>("artist_limit") ?: 2
|
||||
val filter = call.argument<String>("filter") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.searchDeezerAll(query, trackLimit.toLong(), artistLimit.toLong(), filter)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"searchTidalAll" -> {
|
||||
val query = call.argument<String>("query") ?: ""
|
||||
val trackLimit = call.argument<Int>("track_limit") ?: 15
|
||||
|
||||
@@ -1842,24 +1842,6 @@ func ClearTrackIDCache() {
|
||||
ClearTrackCache()
|
||||
}
|
||||
|
||||
func SearchDeezerAll(query string, trackLimit, artistLimit int, filter string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client := GetDeezerClient()
|
||||
results, err := client.SearchAll(ctx, query, trackLimit, artistLimit, filter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(results)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func SearchTidalAll(query string, trackLimit, artistLimit int, filter string) (string, error) {
|
||||
downloader := NewTidalDownloader()
|
||||
results, err := downloader.SearchAll(query, trackLimit, artistLimit, filter)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -895,7 +894,7 @@ func SetMetadataProviderPriority(providerIDs []string) {
|
||||
metadataProviderPriorityMu.Lock()
|
||||
defer metadataProviderPriorityMu.Unlock()
|
||||
|
||||
sanitized := make([]string, 0, len(providerIDs)+3)
|
||||
sanitized := make([]string, 0, len(providerIDs)+2)
|
||||
seen := map[string]struct{}{}
|
||||
for _, providerID := range providerIDs {
|
||||
providerID = strings.TrimSpace(providerID)
|
||||
@@ -908,7 +907,7 @@ func SetMetadataProviderPriority(providerIDs []string) {
|
||||
seen[providerID] = struct{}{}
|
||||
sanitized = append(sanitized, providerID)
|
||||
}
|
||||
for _, providerID := range []string{"deezer", "qobuz", "tidal"} {
|
||||
for _, providerID := range []string{"qobuz", "tidal"} {
|
||||
if _, exists := seen[providerID]; exists {
|
||||
continue
|
||||
}
|
||||
@@ -925,7 +924,7 @@ func GetMetadataProviderPriority() []string {
|
||||
defer metadataProviderPriorityMu.RUnlock()
|
||||
|
||||
if len(metadataProviderPriority) == 0 {
|
||||
return []string{"deezer", "qobuz", "tidal"}
|
||||
return []string{"qobuz", "tidal"}
|
||||
}
|
||||
|
||||
result := make([]string, len(metadataProviderPriority))
|
||||
@@ -935,7 +934,7 @@ func GetMetadataProviderPriority() []string {
|
||||
|
||||
func isBuiltInProvider(providerID string) bool {
|
||||
switch providerID {
|
||||
case "tidal", "qobuz", "deezer":
|
||||
case "tidal", "qobuz":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -1006,20 +1005,6 @@ func metadataTrackDedupKey(track ExtTrackMetadata) string {
|
||||
|
||||
func searchBuiltInMetadataTracks(providerID, query string, limit int) ([]ExtTrackMetadata, error) {
|
||||
switch providerID {
|
||||
case "deezer":
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := GetDeezerClient().SearchAll(ctx, query, limit, 0, "track")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tracks := make([]ExtTrackMetadata, 0, len(results.Tracks))
|
||||
for _, track := range results.Tracks {
|
||||
tracks = append(tracks, normalizeBuiltInMetadataTrack(track, "deezer"))
|
||||
}
|
||||
return tracks, nil
|
||||
case "qobuz":
|
||||
return NewQobuzDownloader().SearchTracks(query, limit)
|
||||
case "tidal":
|
||||
|
||||
@@ -12,7 +12,7 @@ func TestSetMetadataProviderPriorityAddsBuiltIns(t *testing.T) {
|
||||
|
||||
SetMetadataProviderPriority([]string{"tidal"})
|
||||
got := GetMetadataProviderPriority()
|
||||
want := []string{"tidal", "deezer", "qobuz"}
|
||||
want := []string{"tidal", "qobuz"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("unexpected priority length: got %v want %v", got, want)
|
||||
}
|
||||
@@ -208,7 +208,7 @@ func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) {
|
||||
searchBuiltInMetadataTracksFunc = originalSearch
|
||||
}()
|
||||
|
||||
SetMetadataProviderPriority([]string{"qobuz", "tidal", "deezer"})
|
||||
SetMetadataProviderPriority([]string{"qobuz", "tidal"})
|
||||
|
||||
var calls []string
|
||||
searchBuiltInMetadataTracksFunc = func(providerID, query string, limit int) ([]ExtTrackMetadata, error) {
|
||||
@@ -223,10 +223,6 @@ func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) {
|
||||
{ProviderID: "tidal", SpotifyID: "tidal:2", ISRC: "AAA111", Name: "Duplicate"},
|
||||
{ProviderID: "tidal", SpotifyID: "tidal:3", ISRC: "BBB222", Name: "Second"},
|
||||
}, nil
|
||||
case "deezer":
|
||||
return []ExtTrackMetadata{
|
||||
{ProviderID: "deezer", SpotifyID: "deezer:4", ISRC: "CCC333", Name: "Third"},
|
||||
}, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
@@ -237,13 +233,13 @@ func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("SearchTracksWithMetadataProviders returned error: %v", err)
|
||||
}
|
||||
if len(tracks) != 3 {
|
||||
t.Fatalf("unexpected track count: got %d want 3", len(tracks))
|
||||
if len(tracks) != 2 {
|
||||
t.Fatalf("unexpected track count: got %d want 2", len(tracks))
|
||||
}
|
||||
if tracks[0].ProviderID != "qobuz" || tracks[1].ProviderID != "tidal" || tracks[2].ProviderID != "deezer" {
|
||||
if tracks[0].ProviderID != "qobuz" || tracks[1].ProviderID != "tidal" {
|
||||
t.Fatalf("unexpected track provider order: %+v", tracks)
|
||||
}
|
||||
if len(calls) != 3 || calls[0] != "qobuz" || calls[1] != "tidal" || calls[2] != "deezer" {
|
||||
if len(calls) != 2 || calls[0] != "qobuz" || calls[1] != "tidal" {
|
||||
t.Fatalf("unexpected provider call order: %v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,16 +371,6 @@ import Gobackend // Import Go framework
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
case "searchDeezerAll":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let query = args["query"] as! String
|
||||
let trackLimit = args["track_limit"] as? Int ?? 15
|
||||
let artistLimit = args["artist_limit"] as? Int ?? 3
|
||||
let filter = args["filter"] as? String ?? ""
|
||||
let response = GobackendSearchDeezerAll(query, Int(trackLimit), Int(artistLimit), filter, &error)
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
case "searchTidalAll":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let query = args["query"] as! String
|
||||
|
||||
@@ -481,6 +481,7 @@ class ExtensionState {
|
||||
}
|
||||
|
||||
class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
static const _builtInMetadataProviders = ['qobuz', 'tidal'];
|
||||
AppLifecycleListener? _appLifecycleListener;
|
||||
bool _cleanupInFlight = false;
|
||||
Completer<void>? _initializationCompleter;
|
||||
@@ -883,10 +884,15 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
);
|
||||
await PlatformBridge.setMetadataProviderPriority(priority);
|
||||
} else {
|
||||
priority = _sanitizeMetadataProviderPriority(
|
||||
await PlatformBridge.getMetadataProviderPriority(),
|
||||
);
|
||||
final backendPriority =
|
||||
await PlatformBridge.getMetadataProviderPriority();
|
||||
priority = _sanitizeMetadataProviderPriority(backendPriority);
|
||||
_log.d('Using default metadata provider priority: $priority');
|
||||
await prefs.setString(
|
||||
_metadataProviderPriorityKey,
|
||||
jsonEncode(priority),
|
||||
);
|
||||
await PlatformBridge.setMetadataProviderPriority(priority);
|
||||
}
|
||||
|
||||
state = state.copyWith(metadataProviderPriority: priority);
|
||||
@@ -942,17 +948,26 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
|
||||
List<String> getAllMetadataProviders() {
|
||||
final providers = ['deezer', 'qobuz', 'tidal'];
|
||||
for (final ext in state.extensions) {
|
||||
if (ext.enabled && ext.hasMetadataProvider) {
|
||||
providers.add(ext.id);
|
||||
}
|
||||
}
|
||||
return providers;
|
||||
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 [
|
||||
...primarySearchMetadataExtensions,
|
||||
..._builtInMetadataProviders,
|
||||
...otherMetadataExtensions,
|
||||
];
|
||||
}
|
||||
|
||||
List<String> _sanitizeMetadataProviderPriority(List<String> input) {
|
||||
final allowed = getAllMetadataProviders().toSet();
|
||||
final preferredOrder = getAllMetadataProviders();
|
||||
final result = <String>[];
|
||||
|
||||
for (final provider in input) {
|
||||
@@ -961,7 +976,18 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
for (final provider in const ['deezer', 'qobuz', 'tidal']) {
|
||||
final hasPreferredExtension = preferredOrder.any(
|
||||
(provider) => !_builtInMetadataProviders.contains(provider),
|
||||
);
|
||||
final hasSavedExtension = result.any(
|
||||
(provider) => !_builtInMetadataProviders.contains(provider),
|
||||
);
|
||||
|
||||
if (!hasSavedExtension && hasPreferredExtension) {
|
||||
return List<String>.from(preferredOrder);
|
||||
}
|
||||
|
||||
for (final provider in preferredOrder) {
|
||||
if (!result.contains(provider)) {
|
||||
result.add(provider);
|
||||
}
|
||||
|
||||
@@ -583,8 +583,97 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
String? builtInSearchProvider,
|
||||
}) async {
|
||||
final requestId = ++_currentRequestId;
|
||||
|
||||
final currentFilter = filterOverride ?? state.selectedSearchFilter;
|
||||
final settings = ref.read(settingsProvider);
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
|
||||
String? resolvedProvider = builtInSearchProvider;
|
||||
if (resolvedProvider == null || resolvedProvider.isEmpty) {
|
||||
final explicitProvider = settings.searchProvider?.trim();
|
||||
if (explicitProvider != null && explicitProvider.isNotEmpty) {
|
||||
resolvedProvider = explicitProvider;
|
||||
} else {
|
||||
resolvedProvider =
|
||||
extensionState.extensions
|
||||
.where(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasCustomSearch &&
|
||||
ext.searchBehavior?.primary == true,
|
||||
)
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull ??
|
||||
extensionState.extensions
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull;
|
||||
}
|
||||
}
|
||||
|
||||
final isEnabledExtensionProvider =
|
||||
resolvedProvider != null &&
|
||||
resolvedProvider.isNotEmpty &&
|
||||
extensionState.extensions.any(
|
||||
(ext) => ext.enabled && ext.id == resolvedProvider,
|
||||
);
|
||||
|
||||
if (resolvedProvider != null &&
|
||||
resolvedProvider.isNotEmpty &&
|
||||
resolvedProvider != 'tidal' &&
|
||||
resolvedProvider != 'qobuz' &&
|
||||
!isEnabledExtensionProvider &&
|
||||
settings.searchProvider?.trim() == resolvedProvider) {
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(null);
|
||||
resolvedProvider =
|
||||
extensionState.extensions
|
||||
.where(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasCustomSearch &&
|
||||
ext.searchBehavior?.primary == true,
|
||||
)
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull ??
|
||||
extensionState.extensions
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
if (resolvedProvider != null &&
|
||||
resolvedProvider.isNotEmpty &&
|
||||
resolvedProvider != 'tidal' &&
|
||||
resolvedProvider != 'qobuz' &&
|
||||
extensionState.extensions.any(
|
||||
(ext) => ext.enabled && ext.id == resolvedProvider,
|
||||
)) {
|
||||
final resolvedFilter = currentFilter ?? 'track';
|
||||
Map<String, dynamic>? options;
|
||||
options = {'filter': resolvedFilter};
|
||||
await customSearch(
|
||||
resolvedProvider,
|
||||
query,
|
||||
options: options,
|
||||
selectedFilter: resolvedFilter,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final effectiveBuiltInProvider =
|
||||
resolvedProvider == 'tidal' || resolvedProvider == 'qobuz'
|
||||
? resolvedProvider
|
||||
: builtInSearchProvider;
|
||||
|
||||
if (effectiveBuiltInProvider == null || effectiveBuiltInProvider.isEmpty) {
|
||||
state = TrackState(
|
||||
isLoading: false,
|
||||
error: 'No active search provider available',
|
||||
hasSearchText: state.hasSearchText,
|
||||
isShowingRecentAccess: state.isShowingRecentAccess,
|
||||
selectedSearchFilter: currentFilter,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
state = TrackState(
|
||||
isLoading: true,
|
||||
@@ -594,15 +683,13 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
);
|
||||
|
||||
try {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final hasActiveMetadataExtensions = extensionState.extensions.any(
|
||||
(e) => e.enabled && e.hasMetadataProvider,
|
||||
);
|
||||
final includeExtensions =
|
||||
settings.useExtensionProviders && hasActiveMetadataExtensions;
|
||||
|
||||
final effectiveProvider = builtInSearchProvider ?? 'deezer';
|
||||
final effectiveProvider = effectiveBuiltInProvider;
|
||||
|
||||
_log.i(
|
||||
'Search started: provider=$effectiveProvider, query="$query", includeExtensions=$includeExtensions, filter=$currentFilter',
|
||||
@@ -611,25 +698,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
Map<String, dynamic> results;
|
||||
List<Map<String, dynamic>> metadataTrackResults = [];
|
||||
|
||||
if (effectiveProvider == 'deezer') {
|
||||
try {
|
||||
_log.d('Calling metadata provider search API...');
|
||||
metadataTrackResults =
|
||||
await PlatformBridge.searchTracksWithMetadataProviders(
|
||||
query,
|
||||
limit: 20,
|
||||
includeExtensions: includeExtensions,
|
||||
);
|
||||
_log.i(
|
||||
'Metadata providers returned ${metadataTrackResults.length} tracks',
|
||||
);
|
||||
} catch (e) {
|
||||
_log.w(
|
||||
'Metadata provider search failed, falling back to Deezer tracks: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
switch (effectiveProvider) {
|
||||
case 'tidal':
|
||||
_log.d('Calling Tidal search API...');
|
||||
@@ -650,13 +718,19 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
);
|
||||
break;
|
||||
default:
|
||||
_log.d('Calling Deezer search API...');
|
||||
results = await PlatformBridge.searchDeezerAll(
|
||||
query,
|
||||
trackLimit: 20,
|
||||
artistLimit: 2,
|
||||
filter: currentFilter,
|
||||
);
|
||||
_log.d('Calling metadata provider track search API...');
|
||||
metadataTrackResults =
|
||||
await PlatformBridge.searchTracksWithMetadataProviders(
|
||||
query,
|
||||
limit: 20,
|
||||
includeExtensions: includeExtensions,
|
||||
);
|
||||
results = const <String, List<dynamic>>{
|
||||
'tracks': <dynamic>[],
|
||||
'artists': <dynamic>[],
|
||||
'albums': <dynamic>[],
|
||||
'playlists': <dynamic>[],
|
||||
};
|
||||
break;
|
||||
}
|
||||
_log.i(
|
||||
|
||||
+106
-27
@@ -426,14 +426,18 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
String? currentSearchProvider,
|
||||
List<Extension> extensions,
|
||||
) {
|
||||
final resolvedSearchProvider = _resolveSearchProvider(
|
||||
currentSearchProvider,
|
||||
extensions,
|
||||
);
|
||||
final isUsingExtensionSearch =
|
||||
currentSearchProvider != null &&
|
||||
currentSearchProvider.isNotEmpty &&
|
||||
extensions.any((e) => e.id == currentSearchProvider && e.enabled);
|
||||
resolvedSearchProvider != null &&
|
||||
resolvedSearchProvider.isNotEmpty &&
|
||||
extensions.any((e) => e.id == resolvedSearchProvider && e.enabled);
|
||||
|
||||
if (isUsingExtensionSearch) {
|
||||
final currentSearchExtension = extensions
|
||||
.where((e) => e.id == currentSearchProvider && e.enabled)
|
||||
.where((e) => e.id == resolvedSearchProvider && e.enabled)
|
||||
.firstOrNull;
|
||||
final filters = currentSearchExtension?.searchBehavior?.filters;
|
||||
if (filters != null && filters.isNotEmpty) {
|
||||
@@ -449,6 +453,36 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
];
|
||||
}
|
||||
|
||||
Extension? _defaultSearchExtension(List<Extension> extensions) {
|
||||
return extensions
|
||||
.where(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasCustomSearch &&
|
||||
ext.searchBehavior?.primary == true,
|
||||
)
|
||||
.firstOrNull ??
|
||||
extensions
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
String? _resolveSearchProvider(
|
||||
String? explicitSearchProvider,
|
||||
List<Extension> extensions,
|
||||
) {
|
||||
final explicit = explicitSearchProvider?.trim();
|
||||
if (explicit != null &&
|
||||
explicit.isNotEmpty &&
|
||||
(_builtInSearchProviders.contains(explicit) ||
|
||||
extensions.any(
|
||||
(ext) => ext.enabled && ext.hasCustomSearch && ext.id == explicit,
|
||||
))) {
|
||||
return explicit;
|
||||
}
|
||||
return _defaultSearchExtension(extensions)?.id;
|
||||
}
|
||||
|
||||
String? _sanitizeSearchFilterForProvider(
|
||||
String? filter,
|
||||
String? currentSearchProvider,
|
||||
@@ -585,7 +619,10 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
bool _isLiveSearchEnabled() {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final extState = ref.read(extensionProvider);
|
||||
final searchProvider = settings.searchProvider;
|
||||
final searchProvider = _resolveSearchProvider(
|
||||
settings.searchProvider,
|
||||
extState.extensions,
|
||||
);
|
||||
|
||||
if (searchProvider == null || searchProvider.isEmpty) return false;
|
||||
|
||||
@@ -654,7 +691,10 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
Future<void> _performSearch(String query, {String? filterOverride}) async {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final extState = ref.read(extensionProvider);
|
||||
final searchProvider = settings.searchProvider;
|
||||
final searchProvider = _resolveSearchProvider(
|
||||
settings.searchProvider,
|
||||
extState.extensions,
|
||||
);
|
||||
final selectedFilter =
|
||||
_sanitizeSearchFilterForProvider(
|
||||
filterOverride,
|
||||
@@ -2166,17 +2206,25 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
);
|
||||
}
|
||||
|
||||
bool _isEnabledMetadataExtension(String? providerId) {
|
||||
final normalized = providerId?.trim();
|
||||
if (normalized == null || normalized.isEmpty) return false;
|
||||
|
||||
return ref
|
||||
.read(extensionProvider)
|
||||
.extensions
|
||||
.any(
|
||||
(ext) =>
|
||||
ext.enabled && ext.hasMetadataProvider && ext.id == normalized,
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToRecentItem(RecentAccessItem item) {
|
||||
_searchFocusNode.unfocus();
|
||||
|
||||
switch (item.type) {
|
||||
case RecentAccessType.artist:
|
||||
if (item.providerId != null &&
|
||||
item.providerId!.isNotEmpty &&
|
||||
item.providerId != 'deezer' &&
|
||||
item.providerId != 'spotify' &&
|
||||
item.providerId != 'tidal' &&
|
||||
item.providerId != 'qobuz') {
|
||||
if (_isEnabledMetadataExtension(item.providerId)) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
@@ -2213,12 +2261,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (item.providerId != null &&
|
||||
item.providerId!.isNotEmpty &&
|
||||
item.providerId != 'deezer' &&
|
||||
item.providerId != 'spotify' &&
|
||||
item.providerId != 'tidal' &&
|
||||
item.providerId != 'qobuz') {
|
||||
} else if (_isEnabledMetadataExtension(item.providerId)) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
@@ -2263,12 +2306,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.providerId != null &&
|
||||
item.providerId!.isNotEmpty &&
|
||||
item.providerId != 'deezer' &&
|
||||
item.providerId != 'spotify' &&
|
||||
item.providerId != 'tidal' &&
|
||||
item.providerId != 'qobuz') {
|
||||
if (_isEnabledMetadataExtension(item.providerId)) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
@@ -3185,8 +3223,11 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
|
||||
String _getSearchHint() {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final searchProvider = settings.searchProvider;
|
||||
final extState = ref.read(extensionProvider);
|
||||
final searchProvider = _resolveSearchProvider(
|
||||
settings.searchProvider,
|
||||
extState.extensions,
|
||||
);
|
||||
|
||||
if (!extState.isInitialized) {
|
||||
return 'Paste supported URL or search...';
|
||||
@@ -3387,9 +3428,23 @@ class _SearchProviderDropdown extends ConsumerWidget {
|
||||
|
||||
const _SearchProviderDropdown({this.onProviderChanged});
|
||||
|
||||
Extension? _defaultSearchExtension(List<Extension> extensions) {
|
||||
return extensions
|
||||
.where(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasCustomSearch &&
|
||||
ext.searchBehavior?.primary == true,
|
||||
)
|
||||
.firstOrNull ??
|
||||
extensions
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentProvider = ref.watch(
|
||||
final rawCurrentProvider = ref.watch(
|
||||
settingsProvider.select((s) => s.searchProvider),
|
||||
);
|
||||
final extensions = ref.watch(extensionProvider.select((s) => s.extensions));
|
||||
@@ -3398,6 +3453,17 @@ class _SearchProviderDropdown extends ConsumerWidget {
|
||||
final searchProviders = extensions
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.toList();
|
||||
final primarySearchExtension = _defaultSearchExtension(searchProviders);
|
||||
final defaultProviderLabel =
|
||||
primarySearchExtension?.displayName ?? 'Deezer';
|
||||
final defaultProviderIconPath = primarySearchExtension?.iconPath;
|
||||
final currentProvider =
|
||||
rawCurrentProvider != null &&
|
||||
rawCurrentProvider.isNotEmpty &&
|
||||
({'tidal', 'qobuz'}.contains(rawCurrentProvider) ||
|
||||
searchProviders.any((e) => e.id == rawCurrentProvider))
|
||||
? rawCurrentProvider
|
||||
: null;
|
||||
|
||||
Extension? currentExt;
|
||||
if (currentProvider != null && currentProvider.isNotEmpty) {
|
||||
@@ -3417,6 +3483,19 @@ class _SearchProviderDropdown extends ConsumerWidget {
|
||||
if (currentExt.searchBehavior?.icon != null) {
|
||||
displayIcon = _getIconFromName(currentExt.searchBehavior!.icon!);
|
||||
}
|
||||
} else if (primarySearchExtension?.searchBehavior?.icon != null) {
|
||||
displayIcon = _getIconFromName(
|
||||
primarySearchExtension!.searchBehavior!.icon!,
|
||||
);
|
||||
iconPath = defaultProviderIconPath;
|
||||
} else if (defaultProviderIconPath != null &&
|
||||
defaultProviderIconPath.isNotEmpty) {
|
||||
iconPath = defaultProviderIconPath;
|
||||
if (primarySearchExtension?.searchBehavior?.icon != null) {
|
||||
displayIcon = _getIconFromName(
|
||||
primarySearchExtension!.searchBehavior!.icon!,
|
||||
);
|
||||
}
|
||||
} else if (isBuiltInProvider) {
|
||||
displayIcon = Icons.music_note;
|
||||
}
|
||||
@@ -3471,7 +3550,7 @@ class _SearchProviderDropdown extends ConsumerWidget {
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Deezer',
|
||||
defaultProviderLabel,
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
currentProvider == null || currentProvider.isEmpty
|
||||
|
||||
@@ -225,8 +225,8 @@ class _MetadataProviderItem extends StatelessWidget {
|
||||
return _MetadataProviderInfo(
|
||||
name: 'Deezer',
|
||||
icon: Icons.album,
|
||||
description: context.l10n.metadataNoRateLimits,
|
||||
isBuiltIn: true,
|
||||
description: context.l10n.providerExtension,
|
||||
isBuiltIn: false,
|
||||
);
|
||||
case 'qobuz':
|
||||
return _MetadataProviderInfo(
|
||||
|
||||
@@ -719,13 +719,39 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
|
||||
static const _builtInProviders = {'tidal': 'Tidal', 'qobuz': 'Qobuz'};
|
||||
|
||||
Extension? _defaultSearchExtension(List<Extension> extensions) {
|
||||
return extensions
|
||||
.where(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasCustomSearch &&
|
||||
ext.searchBehavior?.primary == true,
|
||||
)
|
||||
.firstOrNull ??
|
||||
extensions
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final settings = ref.watch(settingsProvider);
|
||||
final extState = ref.watch(extensionProvider);
|
||||
|
||||
final searchProvider = settings.searchProvider ?? '';
|
||||
final rawSearchProvider = settings.searchProvider?.trim() ?? '';
|
||||
final isValidBuiltIn = _builtInProviders.containsKey(rawSearchProvider);
|
||||
final primarySearchExtension = _defaultSearchExtension(extState.extensions);
|
||||
final defaultProviderLabel =
|
||||
primarySearchExtension?.displayName ?? 'Deezer';
|
||||
final searchProvider =
|
||||
isValidBuiltIn ||
|
||||
extState.extensions.any(
|
||||
(e) =>
|
||||
e.enabled && e.hasCustomSearch && e.id == rawSearchProvider,
|
||||
)
|
||||
? rawSearchProvider
|
||||
: '';
|
||||
final isBuiltIn = _builtInProviders.containsKey(searchProvider);
|
||||
|
||||
Extension? activeExtension;
|
||||
@@ -772,7 +798,7 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
children: [
|
||||
_SourceChip(
|
||||
icon: Icons.graphic_eq,
|
||||
label: 'Deezer',
|
||||
label: defaultProviderLabel,
|
||||
isSelected: searchProvider.isEmpty,
|
||||
onTap: () {
|
||||
if (hasNonDefaultProvider) {
|
||||
@@ -816,7 +842,7 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Tap Deezer to switch back from extension',
|
||||
'Tap $defaultProviderLabel to switch back from extension',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
||||
@@ -61,32 +61,29 @@ class CsvImportService {
|
||||
if (trackData == null) {
|
||||
try {
|
||||
final query = '${track.artistName} ${track.name}';
|
||||
final searchResult = await PlatformBridge.searchDeezerAll(
|
||||
final searchResult = await PlatformBridge.customSearchWithExtension(
|
||||
'deezer',
|
||||
query,
|
||||
trackLimit: 5,
|
||||
options: {'filter': 'track', 'limit': 5},
|
||||
);
|
||||
|
||||
if (searchResult.containsKey('tracks')) {
|
||||
final tracksList = searchResult['tracks'] as List<dynamic>?;
|
||||
if (tracksList != null && tracksList.isNotEmpty) {
|
||||
for (final result in tracksList) {
|
||||
final resultMap = result as Map<String, dynamic>;
|
||||
final resultName =
|
||||
(resultMap['name'] as String?)?.toLowerCase() ?? '';
|
||||
final trackNameLower = track.name.toLowerCase();
|
||||
if (searchResult.isNotEmpty) {
|
||||
for (final resultMap in searchResult) {
|
||||
final resultName =
|
||||
(resultMap['name'] as String?)?.toLowerCase() ?? '';
|
||||
final trackNameLower = track.name.toLowerCase();
|
||||
|
||||
if (resultName.contains(trackNameLower) ||
|
||||
trackNameLower.contains(resultName)) {
|
||||
trackData = resultMap;
|
||||
_log.d('Text search match for ${track.name}: $resultName');
|
||||
break;
|
||||
}
|
||||
if (resultName.contains(trackNameLower) ||
|
||||
trackNameLower.contains(resultName)) {
|
||||
trackData = resultMap;
|
||||
_log.d('Text search match for ${track.name}: $resultName');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (trackData == null && tracksList.isNotEmpty) {
|
||||
trackData = tracksList.first as Map<String, dynamic>;
|
||||
_log.d('Using first search result for ${track.name}');
|
||||
}
|
||||
if (trackData == null) {
|
||||
trackData = searchResult.first;
|
||||
_log.d('Using first search result for ${track.name}');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -496,21 +496,6 @@ class PlatformBridge {
|
||||
await _channel.invokeMethod('clearTrackCache');
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> searchDeezerAll(
|
||||
String query, {
|
||||
int trackLimit = 15,
|
||||
int artistLimit = 2,
|
||||
String? filter,
|
||||
}) async {
|
||||
final result = await _channel.invokeMethod('searchDeezerAll', {
|
||||
'query': query,
|
||||
'track_limit': trackLimit,
|
||||
'artist_limit': artistLimit,
|
||||
'filter': filter ?? '',
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> searchTidalAll(
|
||||
String query, {
|
||||
int trackLimit = 15,
|
||||
|
||||
@@ -10,6 +10,19 @@ import 'package:spotiflac_android/utils/artist_utils.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
final _log = AppLogger('ClickableMetadata');
|
||||
const _deezerExtensionId = 'deezer';
|
||||
|
||||
Future<List<Map<String, dynamic>>> _searchDeezerExtension(
|
||||
String query, {
|
||||
required String filter,
|
||||
int limit = 5,
|
||||
}) {
|
||||
return PlatformBridge.customSearchWithExtension(
|
||||
_deezerExtensionId,
|
||||
query,
|
||||
options: {'filter': filter, 'limit': limit},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> navigateToArtist(
|
||||
BuildContext context, {
|
||||
@@ -39,15 +52,14 @@ Future<void> navigateToArtist(
|
||||
|
||||
_showLoadingSnackBar(context, 'Looking up artist...');
|
||||
try {
|
||||
final results = await PlatformBridge.searchDeezerAll(
|
||||
final artistList = await _searchDeezerExtension(
|
||||
artistName,
|
||||
trackLimit: 0,
|
||||
artistLimit: 3,
|
||||
filter: 'artist',
|
||||
limit: 3,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
final artistList = results['artists'] as List<dynamic>? ?? [];
|
||||
if (artistList.isEmpty) {
|
||||
_showUnavailable(context, 'Artist');
|
||||
return;
|
||||
@@ -56,15 +68,13 @@ Future<void> navigateToArtist(
|
||||
Map<String, dynamic>? bestMatch;
|
||||
final lowerName = artistName.toLowerCase().trim();
|
||||
for (final a in artistList) {
|
||||
if (a is Map<String, dynamic>) {
|
||||
final name = (a['name'] as String? ?? '').toLowerCase().trim();
|
||||
if (name == lowerName) {
|
||||
bestMatch = a;
|
||||
break;
|
||||
}
|
||||
final name = (a['name'] as String? ?? '').toLowerCase().trim();
|
||||
if (name == lowerName) {
|
||||
bestMatch = a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
bestMatch ??= artistList.first as Map<String, dynamic>;
|
||||
bestMatch ??= artistList.first;
|
||||
|
||||
final resolvedId = bestMatch['id'] as String? ?? '';
|
||||
final resolvedName = bestMatch['name'] as String? ?? artistName;
|
||||
@@ -81,6 +91,7 @@ Future<void> navigateToArtist(
|
||||
artistId: resolvedId,
|
||||
artistName: resolvedName,
|
||||
coverUrl: resolvedImage ?? coverUrl,
|
||||
extensionId: _deezerExtensionId,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.e('Failed to look up artist "$artistName": $e', e);
|
||||
@@ -125,15 +136,14 @@ Future<void> navigateToAlbum(
|
||||
? '$albumName $artistName'
|
||||
: albumName;
|
||||
|
||||
final results = await PlatformBridge.searchDeezerAll(
|
||||
final albumList = await _searchDeezerExtension(
|
||||
query,
|
||||
trackLimit: 0,
|
||||
artistLimit: 0,
|
||||
filter: 'album',
|
||||
limit: 5,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
final albumList = results['albums'] as List<dynamic>? ?? [];
|
||||
if (albumList.isEmpty) {
|
||||
_showUnavailable(context, 'Album');
|
||||
return;
|
||||
@@ -142,15 +152,13 @@ Future<void> navigateToAlbum(
|
||||
Map<String, dynamic>? bestMatch;
|
||||
final lowerName = albumName.toLowerCase().trim();
|
||||
for (final a in albumList) {
|
||||
if (a is Map<String, dynamic>) {
|
||||
final name = (a['name'] as String? ?? '').toLowerCase().trim();
|
||||
if (name == lowerName) {
|
||||
bestMatch = a;
|
||||
break;
|
||||
}
|
||||
final name = (a['name'] as String? ?? '').toLowerCase().trim();
|
||||
if (name == lowerName) {
|
||||
bestMatch = a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
bestMatch ??= albumList.first as Map<String, dynamic>;
|
||||
bestMatch ??= albumList.first;
|
||||
|
||||
final resolvedId = bestMatch['id'] as String? ?? '';
|
||||
final resolvedName = bestMatch['name'] as String? ?? albumName;
|
||||
@@ -167,6 +175,7 @@ Future<void> navigateToAlbum(
|
||||
albumId: resolvedId,
|
||||
albumName: resolvedName,
|
||||
coverUrl: resolvedImage ?? coverUrl,
|
||||
extensionId: _deezerExtensionId,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.e('Failed to look up album "$albumName": $e', e);
|
||||
@@ -207,7 +216,7 @@ void _pushAlbumScreen(
|
||||
String? coverUrl,
|
||||
String? extensionId,
|
||||
}) {
|
||||
const builtInProviders = {'tidal', 'qobuz', 'deezer'};
|
||||
const builtInProviders = {'tidal', 'qobuz'};
|
||||
final isExtension =
|
||||
extensionId != null && !builtInProviders.contains(extensionId);
|
||||
final resolvedExtensionId = extensionId;
|
||||
|
||||
Reference in New Issue
Block a user