mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-20 09:57:15 +02:00
refactor: abstract built-in providers into generic registry and unify platform bridge API
Replace hardcoded Tidal/Qobuz switch/case with builtInProviderSpec registry pattern. Unify searchTidalAll/searchQobuzAll into searchProviderAll, getDeezerMetadata/getTidalMetadata/getQobuzMetadata into getProviderMetadata, and parseDeezerUrl/parseQobuzUrl/parseTidalUrl into parseProviderUrl. Remove extension-specific getAlbum/Playlist/ArtistWithExtension in favor of generic getProviderMetadata routing. Extract provider UI helpers into provider_ui_utils.dart. Preserve track_number fallback for zero-value TrackNumber in album/playlist track lists.
This commit is contained in:
@@ -2575,9 +2575,11 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final providerTrackId = track.id.substring(colonIdx + 1);
|
||||
|
||||
_log.d('No ISRC, fetching from $provider API: $providerTrackId');
|
||||
final providerData = provider == 'tidal'
|
||||
? await PlatformBridge.getTidalMetadata('track', providerTrackId)
|
||||
: await PlatformBridge.getQobuzMetadata('track', providerTrackId);
|
||||
final providerData = await PlatformBridge.getProviderMetadata(
|
||||
provider,
|
||||
'track',
|
||||
providerTrackId,
|
||||
);
|
||||
|
||||
final trackData = providerData['track'] as Map<String, dynamic>?;
|
||||
if (trackData == null) {
|
||||
@@ -4448,7 +4450,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
);
|
||||
final rawId = trackToDownload.id.split(':')[1];
|
||||
_log.d('Fetching full metadata for Deezer ID: $rawId');
|
||||
final fullData = await PlatformBridge.getDeezerMetadata(
|
||||
final fullData = await PlatformBridge.getProviderMetadata(
|
||||
'deezer',
|
||||
'track',
|
||||
rawId,
|
||||
);
|
||||
|
||||
@@ -15,6 +15,44 @@ const _metadataProviderPriorityKey = 'metadata_provider_priority';
|
||||
const _providerPriorityKey = 'provider_priority';
|
||||
const _spotifyWebExtensionId = 'spotify-web';
|
||||
|
||||
class BuiltInProviderSpec {
|
||||
final String id;
|
||||
final String displayName;
|
||||
final bool supportsMetadata;
|
||||
final bool supportsDownload;
|
||||
final bool supportsSearch;
|
||||
|
||||
const BuiltInProviderSpec({
|
||||
required this.id,
|
||||
required this.displayName,
|
||||
this.supportsMetadata = false,
|
||||
this.supportsDownload = false,
|
||||
this.supportsSearch = false,
|
||||
});
|
||||
|
||||
factory BuiltInProviderSpec.fromJson(Map<String, dynamic> json) {
|
||||
return BuiltInProviderSpec(
|
||||
id: json['id'] as String? ?? '',
|
||||
displayName:
|
||||
json['display_name'] as String? ??
|
||||
json['displayName'] as String? ??
|
||||
'',
|
||||
supportsMetadata: json['supports_metadata'] as bool? ?? false,
|
||||
supportsDownload: json['supports_download'] as bool? ?? false,
|
||||
supportsSearch: json['supports_search'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<BuiltInProviderSpec> _builtInProviderRegistry = const [];
|
||||
|
||||
List<BuiltInProviderSpec> get builtInProviderSpecs =>
|
||||
List<BuiltInProviderSpec>.unmodifiable(_builtInProviderRegistry);
|
||||
|
||||
void _replaceBuiltInProviderRegistry(List<BuiltInProviderSpec> providers) {
|
||||
_builtInProviderRegistry = List<BuiltInProviderSpec>.unmodifiable(providers);
|
||||
}
|
||||
|
||||
class Extension {
|
||||
final String id;
|
||||
final String name;
|
||||
@@ -195,6 +233,83 @@ class Extension {
|
||||
}
|
||||
}
|
||||
|
||||
BuiltInProviderSpec? builtInProviderSpecForId(String? providerId) {
|
||||
if (providerId == null) return null;
|
||||
|
||||
for (final provider in builtInProviderSpecs) {
|
||||
if (provider.id == providerId) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
List<BuiltInProviderSpec> _builtInProvidersWhere(
|
||||
bool Function(BuiltInProviderSpec provider) predicate,
|
||||
) {
|
||||
return List<BuiltInProviderSpec>.unmodifiable(
|
||||
builtInProviderSpecs.where(predicate),
|
||||
);
|
||||
}
|
||||
|
||||
List<BuiltInProviderSpec> get builtInSearchProviderSpecs =>
|
||||
_builtInProvidersWhere((provider) => provider.supportsSearch);
|
||||
|
||||
List<BuiltInProviderSpec> get builtInMetadataProviderSpecs =>
|
||||
_builtInProvidersWhere((provider) => provider.supportsMetadata);
|
||||
|
||||
List<BuiltInProviderSpec> get builtInDownloadProviderSpecs =>
|
||||
_builtInProvidersWhere((provider) => provider.supportsDownload);
|
||||
|
||||
List<String> get builtInSearchProviderIds => List<String>.unmodifiable(
|
||||
builtInSearchProviderSpecs.map((provider) => provider.id),
|
||||
);
|
||||
|
||||
List<String> get builtInMetadataProviderIds => List<String>.unmodifiable(
|
||||
builtInMetadataProviderSpecs.map((provider) => provider.id),
|
||||
);
|
||||
|
||||
List<String> get builtInDownloadProviderIds => List<String>.unmodifiable(
|
||||
builtInDownloadProviderSpecs.map((provider) => provider.id),
|
||||
);
|
||||
|
||||
bool isBuiltInSearchProvider(String? providerId) =>
|
||||
builtInProviderSpecForId(providerId)?.supportsSearch ?? false;
|
||||
|
||||
bool isBuiltInMetadataProvider(String? providerId) =>
|
||||
builtInProviderSpecForId(providerId)?.supportsMetadata ?? false;
|
||||
|
||||
bool isBuiltInDownloadProvider(String? providerId) =>
|
||||
builtInProviderSpecForId(providerId)?.supportsDownload ?? false;
|
||||
|
||||
String? get defaultBuiltInSearchProviderId => builtInSearchProviderSpecs.isEmpty
|
||||
? null
|
||||
: builtInSearchProviderSpecs.first.id;
|
||||
|
||||
String? get defaultBuiltInSearchProviderDisplayName =>
|
||||
builtInSearchProviderSpecs.isEmpty
|
||||
? null
|
||||
: builtInSearchProviderSpecs.first.displayName;
|
||||
|
||||
String resolveProviderDisplayName(
|
||||
String providerId, {
|
||||
Iterable<Extension> extensions = const [],
|
||||
}) {
|
||||
final builtIn = builtInProviderSpecForId(providerId);
|
||||
if (builtIn != null) {
|
||||
return builtIn.displayName;
|
||||
}
|
||||
|
||||
for (final extension in extensions) {
|
||||
if (extension.id == providerId) {
|
||||
return extension.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
return providerId;
|
||||
}
|
||||
|
||||
class SearchFilter {
|
||||
final String id;
|
||||
final String? label;
|
||||
@@ -460,6 +575,7 @@ class ExtensionSetting {
|
||||
|
||||
class ExtensionState {
|
||||
final List<Extension> extensions;
|
||||
final List<BuiltInProviderSpec> builtInProviders;
|
||||
final List<String> providerPriority;
|
||||
final List<String> metadataProviderPriority;
|
||||
final bool isLoading;
|
||||
@@ -468,6 +584,7 @@ class ExtensionState {
|
||||
|
||||
const ExtensionState({
|
||||
this.extensions = const [],
|
||||
this.builtInProviders = const [],
|
||||
this.providerPriority = const [],
|
||||
this.metadataProviderPriority = const [],
|
||||
this.isLoading = false,
|
||||
@@ -477,6 +594,7 @@ class ExtensionState {
|
||||
|
||||
ExtensionState copyWith({
|
||||
List<Extension>? extensions,
|
||||
List<BuiltInProviderSpec>? builtInProviders,
|
||||
List<String>? providerPriority,
|
||||
List<String>? metadataProviderPriority,
|
||||
bool? isLoading,
|
||||
@@ -485,6 +603,7 @@ class ExtensionState {
|
||||
}) {
|
||||
return ExtensionState(
|
||||
extensions: extensions ?? this.extensions,
|
||||
builtInProviders: builtInProviders ?? this.builtInProviders,
|
||||
providerPriority: providerPriority ?? this.providerPriority,
|
||||
metadataProviderPriority:
|
||||
metadataProviderPriority ?? this.metadataProviderPriority,
|
||||
@@ -496,7 +615,6 @@ class ExtensionState {
|
||||
}
|
||||
|
||||
class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
static const _builtInMetadataProviders = ['qobuz', 'tidal'];
|
||||
AppLifecycleListener? _appLifecycleListener;
|
||||
bool _cleanupInFlight = false;
|
||||
Completer<void>? _initializationCompleter;
|
||||
@@ -547,6 +665,12 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
await refreshBuiltInProviders();
|
||||
} catch (e) {
|
||||
_log.w('Failed to refresh built-in providers before init: $e');
|
||||
}
|
||||
|
||||
if (!PlatformBridge.supportsExtensionSystem) {
|
||||
state = state.copyWith(
|
||||
isInitialized: true,
|
||||
@@ -634,6 +758,16 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshBuiltInProviders() async {
|
||||
final list = await PlatformBridge.getBuiltInProviders();
|
||||
final providers = list
|
||||
.map((e) => BuiltInProviderSpec.fromJson(e))
|
||||
.where((provider) => provider.id.isNotEmpty)
|
||||
.toList();
|
||||
_replaceBuiltInProviderRegistry(providers);
|
||||
state = state.copyWith(builtInProviders: providers);
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
state = state.copyWith(error: null);
|
||||
}
|
||||
@@ -727,10 +861,16 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
|
||||
if (ext.hasDownloadProvider && settings.defaultService == extensionId) {
|
||||
ref.read(settingsProvider.notifier).setDefaultService('tidal');
|
||||
_log.d(
|
||||
'Reset default service to Tidal because extension $extensionId was disabled',
|
||||
);
|
||||
final availableProviders = getAllDownloadProviders();
|
||||
if (availableProviders.isNotEmpty) {
|
||||
final fallbackService = availableProviders.first;
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDefaultService(fallbackService);
|
||||
_log.d(
|
||||
'Reset default service to $fallbackService because extension $extensionId was disabled',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -973,7 +1113,7 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
|
||||
List<String> getAllDownloadProviders() {
|
||||
final providers = ['tidal', 'qobuz'];
|
||||
final providers = List<String>.from(builtInDownloadProviderIds);
|
||||
for (final ext in state.extensions) {
|
||||
if (ext.enabled && ext.hasDownloadProvider) {
|
||||
providers.add(ext.id);
|
||||
@@ -995,7 +1135,7 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
|
||||
return [
|
||||
...primarySearchMetadataExtensions,
|
||||
..._builtInMetadataProviders,
|
||||
...builtInMetadataProviderIds,
|
||||
...otherMetadataExtensions,
|
||||
];
|
||||
}
|
||||
@@ -1012,10 +1152,10 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
|
||||
final hasPreferredExtension = preferredOrder.any(
|
||||
(provider) => !_builtInMetadataProviders.contains(provider),
|
||||
(provider) => !isBuiltInMetadataProvider(provider),
|
||||
);
|
||||
final hasSavedExtension = result.any(
|
||||
(provider) => !_builtInMetadataProviders.contains(provider),
|
||||
(provider) => !isBuiltInMetadataProvider(provider),
|
||||
);
|
||||
|
||||
if (!hasSavedExtension && hasPreferredExtension) {
|
||||
|
||||
+178
-259
@@ -353,212 +353,12 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
}
|
||||
}
|
||||
|
||||
if (url.contains('deezer.com') || url.contains('deezer.page.link')) {
|
||||
_log.i('Detected Deezer URL, parsing...');
|
||||
final parsed = await PlatformBridge.parseDeezerUrl(url);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
final type = parsed['type'] as String;
|
||||
final id = parsed['id'] as String;
|
||||
|
||||
final metadata = await PlatformBridge.getDeezerMetadata(type, id);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
if (type == 'track') {
|
||||
final trackData = metadata['track'] as Map<String, dynamic>;
|
||||
final track = _parseTrack(trackData);
|
||||
state = TrackState(
|
||||
tracks: [track],
|
||||
isLoading: false,
|
||||
coverUrl: track.coverUrl,
|
||||
);
|
||||
} else if (type == 'album') {
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
albumId: id,
|
||||
albumName: albumInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(albumInfo['images']?.toString()),
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
} else if (type == 'playlist') {
|
||||
final playlistInfo =
|
||||
metadata['playlist_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
playlistName: playlistInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(
|
||||
playlistInfo['images']?.toString(),
|
||||
),
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
} else if (type == 'artist') {
|
||||
final artistInfo = metadata['artist_info'] as Map<String, dynamic>;
|
||||
final albumsList = metadata['albums'] as List<dynamic>;
|
||||
final albums = albumsList
|
||||
.map((a) => _parseArtistAlbum(a as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = TrackState(
|
||||
tracks: [],
|
||||
isLoading: false,
|
||||
artistId: artistInfo['id'] as String?,
|
||||
artistName: artistInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(artistInfo['images']?.toString()),
|
||||
artistAlbums: albums,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.contains('qobuz.com') || url.startsWith('qobuzapp://')) {
|
||||
_log.i('Detected Qobuz URL, parsing...');
|
||||
final parsed = await PlatformBridge.parseQobuzUrl(url);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
final type = parsed['type'] as String;
|
||||
final id = parsed['id'] as String;
|
||||
|
||||
final metadata = await PlatformBridge.getQobuzMetadata(type, id);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
if (type == 'track') {
|
||||
final trackData = metadata['track'] as Map<String, dynamic>;
|
||||
final track = _parseTrack(trackData);
|
||||
state = TrackState(
|
||||
tracks: [track],
|
||||
isLoading: false,
|
||||
coverUrl: track.coverUrl,
|
||||
);
|
||||
} else if (type == 'album') {
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
albumId: 'qobuz:$id',
|
||||
albumName: albumInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(albumInfo['images']?.toString()),
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
} else if (type == 'playlist') {
|
||||
final playlistInfo =
|
||||
metadata['playlist_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
final owner = playlistInfo['owner'] as Map<String, dynamic>?;
|
||||
final playlistName =
|
||||
(playlistInfo['name'] ?? owner?['name']) as String?;
|
||||
final coverUrl = normalizeRemoteHttpUrl(
|
||||
(playlistInfo['images'] ?? owner?['images'])?.toString(),
|
||||
);
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
playlistName: playlistName,
|
||||
coverUrl: coverUrl,
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
} else if (type == 'artist') {
|
||||
final artistInfo = metadata['artist_info'] as Map<String, dynamic>;
|
||||
final albumsList = metadata['albums'] as List<dynamic>;
|
||||
final albums = albumsList
|
||||
.map((a) => _parseArtistAlbum(a as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = TrackState(
|
||||
tracks: [],
|
||||
isLoading: false,
|
||||
artistId: artistInfo['id'] as String?,
|
||||
artistName: artistInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(artistInfo['images']?.toString()),
|
||||
artistAlbums: albums,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.contains('tidal.com')) {
|
||||
_log.i('Detected Tidal URL, parsing...');
|
||||
final parsed = await PlatformBridge.parseTidalUrl(url);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
final type = parsed['type'] as String;
|
||||
final id = parsed['id'] as String;
|
||||
|
||||
final metadata = await PlatformBridge.getTidalMetadata(type, id);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
if (type == 'track') {
|
||||
final trackData = metadata['track'] as Map<String, dynamic>;
|
||||
final track = _parseTrack(trackData);
|
||||
state = TrackState(
|
||||
tracks: [track],
|
||||
isLoading: false,
|
||||
coverUrl: track.coverUrl,
|
||||
);
|
||||
} else if (type == 'album') {
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
albumId: 'tidal:$id',
|
||||
albumName: albumInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(albumInfo['images']?.toString()),
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
} else if (type == 'playlist') {
|
||||
final playlistInfo =
|
||||
metadata['playlist_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
final owner = playlistInfo['owner'] as Map<String, dynamic>?;
|
||||
final playlistName =
|
||||
(playlistInfo['name'] ?? owner?['name']) as String?;
|
||||
final coverUrl = normalizeRemoteHttpUrl(
|
||||
(playlistInfo['images'] ?? owner?['images'])?.toString(),
|
||||
);
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
playlistName: playlistName,
|
||||
coverUrl: coverUrl,
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
} else if (type == 'artist') {
|
||||
final artistInfo = metadata['artist_info'] as Map<String, dynamic>;
|
||||
final albumsList = metadata['albums'] as List<dynamic>;
|
||||
final albums = albumsList
|
||||
.map((a) => _parseArtistAlbum(a as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = TrackState(
|
||||
tracks: [],
|
||||
isLoading: false,
|
||||
artistId: artistInfo['id'] as String?,
|
||||
artistName: artistInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(artistInfo['images']?.toString()),
|
||||
artistAlbums: albums,
|
||||
);
|
||||
}
|
||||
final handledBuiltInUrl = await _tryResolveBuiltInProviderUrl(
|
||||
url,
|
||||
requestId,
|
||||
);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
if (handledBuiltInUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -577,6 +377,134 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _tryResolveBuiltInProviderUrl(String url, int requestId) async {
|
||||
Map<String, dynamic> parsed;
|
||||
try {
|
||||
parsed = await PlatformBridge.parseProviderUrl(url);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_isRequestValid(requestId)) return true;
|
||||
|
||||
final providerId = parsed['provider_id']?.toString();
|
||||
final type = parsed['type']?.toString();
|
||||
final id = parsed['id']?.toString();
|
||||
if (providerId == null ||
|
||||
providerId.isEmpty ||
|
||||
type == null ||
|
||||
type.isEmpty ||
|
||||
id == null ||
|
||||
id.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_log.i('Detected built-in provider URL: $providerId:$type:$id');
|
||||
|
||||
final metadata = await _getResolvedProviderMetadata(providerId, type, id);
|
||||
if (!_isRequestValid(requestId)) return true;
|
||||
|
||||
_applyResolvedProviderMetadata(providerId, type, id, metadata);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _getResolvedProviderMetadata(
|
||||
String providerId,
|
||||
String resourceType,
|
||||
String resourceId,
|
||||
) async {
|
||||
return PlatformBridge.getProviderMetadata(
|
||||
providerId,
|
||||
resourceType,
|
||||
resourceId,
|
||||
);
|
||||
}
|
||||
|
||||
void _applyResolvedProviderMetadata(
|
||||
String providerId,
|
||||
String resourceType,
|
||||
String resourceId,
|
||||
Map<String, dynamic> metadata,
|
||||
) {
|
||||
switch (resourceType) {
|
||||
case 'track':
|
||||
final trackData = metadata['track'] as Map<String, dynamic>;
|
||||
final track = _parseTrack(trackData);
|
||||
state = TrackState(
|
||||
tracks: [track],
|
||||
isLoading: false,
|
||||
coverUrl: track.coverUrl,
|
||||
);
|
||||
return;
|
||||
case 'album':
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
albumId: _buildResolvedAlbumId(providerId, resourceId),
|
||||
albumName: albumInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(albumInfo['images']?.toString()),
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
return;
|
||||
case 'playlist':
|
||||
final playlistInfo = metadata['playlist_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
final owner = playlistInfo['owner'] as Map<String, dynamic>?;
|
||||
final playlistName =
|
||||
(playlistInfo['name'] ?? owner?['name']) as String?;
|
||||
final coverUrl = normalizeRemoteHttpUrl(
|
||||
(playlistInfo['images'] ?? owner?['images'])?.toString(),
|
||||
);
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
playlistName: playlistName,
|
||||
coverUrl: coverUrl,
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
return;
|
||||
case 'artist':
|
||||
final artistInfo = metadata['artist_info'] as Map<String, dynamic>;
|
||||
final albumsList = metadata['albums'] as List<dynamic>;
|
||||
final albums = albumsList
|
||||
.map((a) => _parseArtistAlbum(a as Map<String, dynamic>))
|
||||
.toList();
|
||||
final topTracksList = metadata['top_tracks'] as List<dynamic>? ?? [];
|
||||
final topTracks = topTracksList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = TrackState(
|
||||
tracks: [],
|
||||
isLoading: false,
|
||||
artistId: artistInfo['id'] as String?,
|
||||
artistName: artistInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(artistInfo['images']?.toString()),
|
||||
headerImageUrl: normalizeRemoteHttpUrl(
|
||||
(artistInfo['header_image'] ?? artistInfo['cover_url'])?.toString(),
|
||||
),
|
||||
monthlyListeners: artistInfo['listeners'] as int?,
|
||||
artistAlbums: albums,
|
||||
artistTopTracks: topTracks.isNotEmpty ? topTracks : null,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String _buildResolvedAlbumId(String providerId, String resourceId) {
|
||||
if (providerId == 'deezer') {
|
||||
return resourceId;
|
||||
}
|
||||
return '$providerId:$resourceId';
|
||||
}
|
||||
|
||||
Future<void> search(
|
||||
String query, {
|
||||
String? filterOverride,
|
||||
@@ -609,19 +537,15 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull;
|
||||
}
|
||||
resolvedProvider ??= 'tidal';
|
||||
resolvedProvider ??= defaultBuiltInSearchProviderId;
|
||||
}
|
||||
|
||||
final isEnabledExtensionProvider =
|
||||
if (resolvedProvider != null &&
|
||||
resolvedProvider.isNotEmpty &&
|
||||
extensionState.extensions.any(
|
||||
!isBuiltInSearchProvider(resolvedProvider) &&
|
||||
!extensionState.extensions.any(
|
||||
(ext) => ext.enabled && ext.id == resolvedProvider,
|
||||
);
|
||||
|
||||
if (resolvedProvider.isNotEmpty &&
|
||||
resolvedProvider != 'tidal' &&
|
||||
resolvedProvider != 'qobuz' &&
|
||||
!isEnabledExtensionProvider &&
|
||||
) &&
|
||||
settings.searchProvider?.trim() == resolvedProvider) {
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(null);
|
||||
resolvedProvider =
|
||||
@@ -638,15 +562,21 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull;
|
||||
resolvedProvider ??= 'tidal';
|
||||
resolvedProvider ??= defaultBuiltInSearchProviderId;
|
||||
}
|
||||
|
||||
if (resolvedProvider.isNotEmpty &&
|
||||
resolvedProvider != 'tidal' &&
|
||||
resolvedProvider != 'qobuz' &&
|
||||
final isEnabledExtensionProvider =
|
||||
resolvedProvider != null &&
|
||||
resolvedProvider.isNotEmpty &&
|
||||
extensionState.extensions.any(
|
||||
(ext) => ext.enabled && ext.id == resolvedProvider,
|
||||
)) {
|
||||
);
|
||||
final isBuiltInProvider = isBuiltInSearchProvider(resolvedProvider);
|
||||
|
||||
if (resolvedProvider != null &&
|
||||
resolvedProvider.isNotEmpty &&
|
||||
!isBuiltInProvider &&
|
||||
isEnabledExtensionProvider) {
|
||||
final resolvedFilter = requestFilter ?? 'track';
|
||||
Map<String, dynamic>? options;
|
||||
options = {'filter': resolvedFilter};
|
||||
@@ -659,12 +589,12 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
return;
|
||||
}
|
||||
|
||||
final effectiveBuiltInProvider =
|
||||
resolvedProvider == 'tidal' || resolvedProvider == 'qobuz'
|
||||
final fallbackBuiltInProvider = builtInSearchProvider?.isNotEmpty == true
|
||||
? builtInSearchProvider
|
||||
: defaultBuiltInSearchProviderId;
|
||||
final effectiveBuiltInProvider = isBuiltInProvider
|
||||
? resolvedProvider
|
||||
: (builtInSearchProvider?.isNotEmpty == true
|
||||
? builtInSearchProvider
|
||||
: 'tidal');
|
||||
: fallbackBuiltInProvider;
|
||||
|
||||
if (effectiveBuiltInProvider == null || effectiveBuiltInProvider.isEmpty) {
|
||||
state = TrackState(
|
||||
@@ -700,40 +630,29 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
Map<String, dynamic> results;
|
||||
List<Map<String, dynamic>> metadataTrackResults = [];
|
||||
|
||||
switch (effectiveProvider) {
|
||||
case 'tidal':
|
||||
_log.d('Calling Tidal search API...');
|
||||
results = await PlatformBridge.searchTidalAll(
|
||||
query,
|
||||
trackLimit: 20,
|
||||
artistLimit: 2,
|
||||
filter: requestFilter,
|
||||
);
|
||||
break;
|
||||
case 'qobuz':
|
||||
_log.d('Calling Qobuz search API...');
|
||||
results = await PlatformBridge.searchQobuzAll(
|
||||
query,
|
||||
trackLimit: 20,
|
||||
artistLimit: 2,
|
||||
filter: requestFilter,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
_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;
|
||||
if (isBuiltInSearchProvider(effectiveProvider)) {
|
||||
_log.d('Calling built-in search API for $effectiveProvider...');
|
||||
results = await PlatformBridge.searchProviderAll(
|
||||
effectiveProvider,
|
||||
query,
|
||||
trackLimit: 20,
|
||||
artistLimit: 2,
|
||||
filter: requestFilter,
|
||||
);
|
||||
} else {
|
||||
_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>[],
|
||||
};
|
||||
}
|
||||
_log.i(
|
||||
'$effectiveProvider returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums',
|
||||
|
||||
Reference in New Issue
Block a user