fix(metadata): support extension custom search

This commit is contained in:
zarzet
2026-07-30 15:36:31 +07:00
parent d85c8f6a17
commit 3e41aa0eee
6 changed files with 102 additions and 54 deletions
@@ -1868,18 +1868,11 @@ class MainActivity: FlutterFragmentActivity() {
val query = call.argument<String>("query") ?: ""
val limit = call.argument<Int>("limit") ?: 20
val response = withContext(Dispatchers.IO) {
val method = Gobackend::class.java.getMethod(
"searchTracksWithMetadataProviderJSON",
String::class.java,
String::class.java,
java.lang.Long.TYPE
)
method.invoke(
null,
Gobackend.searchTracksWithMetadataProviderJSON(
extensionId,
query,
limit.toLong()
) as? String ?: "[]"
)
}
result.success(response)
}
+31
View File
@@ -361,6 +361,20 @@ class SearchFilter {
}
}
String canonicalExtensionSearchFilterId(String value) {
final normalized = value.trim().toLowerCase().replaceAll(
RegExp(r'[^a-z0-9]+'),
'',
);
return switch (normalized) {
'track' || 'tracks' || 'song' || 'songs' || 'music' => 'track',
'artist' || 'artists' => 'artist',
'album' || 'albums' => 'album',
'playlist' || 'playlists' => 'playlist',
_ => normalized,
};
}
class SearchBehavior {
final bool enabled;
final String? placeholder;
@@ -399,6 +413,23 @@ class SearchBehavior {
);
}
String? filterIdForKind(String kind) {
final canonicalKind = canonicalExtensionSearchFilterId(kind);
for (final filter in filters) {
if (canonicalExtensionSearchFilterId(filter.id) == canonicalKind ||
(filter.label != null &&
canonicalExtensionSearchFilterId(filter.label!) ==
canonicalKind) ||
(filter.icon != null &&
canonicalExtensionSearchFilterId(filter.icon!) ==
canonicalKind)) {
final id = filter.id.trim();
if (id.isNotEmpty) return id;
}
}
return null;
}
(double, double) getThumbnailSize({double defaultSize = 56}) {
if (thumbnailWidth != null && thumbnailHeight != null) {
return (thumbnailWidth!.toDouble(), thumbnailHeight!.toDouble());
+3 -24
View File
@@ -183,32 +183,11 @@ class HomeSearchProviderPolicy {
return null;
}
return filters
.where(
(candidate) =>
canonicalFilterId(candidate.id) == canonicalFilter ||
(candidate.label != null &&
canonicalFilterId(candidate.label!) == canonicalFilter) ||
(candidate.icon != null &&
canonicalFilterId(candidate.icon!) == canonicalFilter),
)
.firstOrNull
?.id;
return extension?.searchBehavior?.filterIdForKind(canonicalFilter);
}
static String canonicalFilterId(String value) {
final normalized = value.trim().toLowerCase().replaceAll(
RegExp(r'[^a-z0-9]+'),
'',
);
return switch (normalized) {
'track' || 'tracks' || 'song' || 'songs' || 'music' => 'track',
'artist' || 'artists' => 'artist',
'album' || 'albums' => 'album',
'playlist' || 'playlists' => 'playlist',
_ => normalized,
};
}
static String canonicalFilterId(String value) =>
canonicalExtensionSearchFilterId(value);
static String? preferredFilter(
String preferredSearchTab,
+38 -21
View File
@@ -870,16 +870,17 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
context,
listen: false,
).read(extensionProvider);
final selectedProviderId =
configuredProviderId != null &&
extensionState.extensions.any(
(extension) =>
extension.id == configuredProviderId &&
extension.enabled &&
extension.hasMetadataProvider,
)
? configuredProviderId
: null;
final selectedProvider = configuredProviderId == null
? null
: extensionState.extensions
.where(
(extension) =>
extension.id == configuredProviderId &&
extension.enabled &&
extension.hasMetadataProvider,
)
.firstOrNull;
final selectedProviderId = selectedProvider?.id;
final usesAutomaticProvider =
selectedProviderId == null || selectedProviderId.isEmpty;
final shouldFetchLyrics = _autoFillFields.contains('lyrics');
@@ -920,16 +921,31 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
if (needsTrackLookup && best == null) {
final query = queryParts.join(' ');
final results = usesAutomaticProvider
? await PlatformBridge.searchTracksWithMetadataProviders(
query,
limit: 5,
)
: await PlatformBridge.searchTracksWithMetadataProvider(
selectedProviderId,
query,
limit: 5,
);
final List<Map<String, dynamic>> results;
if (usesAutomaticProvider) {
results = await PlatformBridge.searchTracksWithMetadataProviders(
query,
limit: 5,
);
} else if (selectedProvider!.hasCustomSearch) {
final trackFilter = selectedProvider.searchBehavior?.filterIdForKind(
'track',
);
results = await PlatformBridge.customSearchWithExtension(
selectedProvider.id,
query,
options: {
'limit': 5,
if (trackFilter != null) 'filter': trackFilter,
},
);
} else {
results = await PlatformBridge.searchTracksWithMetadataProvider(
selectedProvider!.id,
query,
limit: 5,
);
}
if (!mounted) return;
@@ -1186,7 +1202,8 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
coverUrl: coverUrl?.isNotEmpty == true ? coverUrl : null,
);
});
} catch (e) {
} catch (e, stackTrace) {
_log.e('Metadata auto-fill failed: $e', e, stackTrace);
if (mounted) {
_showSheetSnackBar(
context.l10n.snackbarError(context.friendlyError(e)),
+2
View File
@@ -173,6 +173,8 @@ void main() {
]),
'songs',
);
expect(extension.searchBehavior?.filterIdForKind('track'), 'songs');
expect(extension.searchBehavior?.filterIdForKind('album'), 'records');
expect(
HomeSearchProviderPolicy.displayFilterSelection(
null,
+26
View File
@@ -104,4 +104,30 @@ void main() {
});
expect(results.single['provider_id'], 'selected-metadata');
});
test('metadata custom search sends the manifest track filter', () async {
MethodCall? capturedCall;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(backendChannel, (call) async {
capturedCall = call;
return jsonEncode([
{'id': 'track-1', 'name': 'Song', 'provider_id': 'custom-metadata'},
]);
});
final results = await PlatformBridge.customSearchWithExtension(
'custom-metadata',
'Song Artist',
options: {'limit': 5, 'filter': 'songs'},
);
expect(capturedCall?.method, 'customSearchWithExtension');
final arguments = capturedCall?.arguments as Map<Object?, Object?>?;
expect(arguments, isNotNull);
expect(arguments?['extension_id'], 'custom-metadata');
expect(arguments?['query'], 'Song Artist');
expect(arguments?['options'], jsonEncode({'limit': 5, 'filter': 'songs'}));
expect(arguments?['request_id'], isA<String>());
expect(results.single['provider_id'], 'custom-metadata');
});
}