fix(metadata): prompt verification during autofill #511

This commit is contained in:
zarzet
2026-08-11 15:38:39 +07:00
parent 33db0cf297
commit d5bfc1e365
5 changed files with 137 additions and 22 deletions
+11
View File
@@ -128,6 +128,7 @@ func (m *extensionManager) SearchTracksWithMetadataProvidersForItemID(query stri
tracks := make([]ExtTrackMetadata, 0, limit)
seenTracks := make(map[string]struct{})
var verificationErr error
for _, providerID := range orderedProviderIDs {
if isDownloadCancelled(itemID) {
return nil, ErrDownloadCancelled
@@ -150,6 +151,13 @@ func (m *extensionManager) SearchTracksWithMetadataProvidersForItemID(query stri
if errors.Is(err, ErrDownloadCancelled) {
return nil, ErrDownloadCancelled
}
if verificationErr == nil && strings.EqualFold(classifyDownloadErrorType(err.Error()), "verification_required") {
verificationErr = fmt.Errorf(
"verification_required: extension '%s' needs verification: %w",
providerID,
err,
)
}
GoLog("[MetadataSearch] Search error from %s: %v\n", providerID, err)
continue
}
@@ -169,6 +177,9 @@ func (m *extensionManager) SearchTracksWithMetadataProvidersForItemID(query stri
}
}
}
if len(tracks) == 0 && verificationErr != nil {
return nil, verificationErr
}
return tracks, nil
}
+70 -22
View File
@@ -991,6 +991,70 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
return false;
}
Future<List<Map<String, dynamic>>> _searchAutoFillCandidates({
required String query,
required bool usesAutomaticProvider,
required Extension? selectedProvider,
required ExtensionState extensionState,
bool allowVerificationRetry = true,
}) async {
try {
if (usesAutomaticProvider) {
return await PlatformBridge.searchTracksWithMetadataProviders(
query,
limit: 5,
);
}
final provider = selectedProvider!;
if (provider.hasCustomSearch) {
final trackFilter = provider.searchBehavior?.filterIdForKind('track');
return await PlatformBridge.customSearchWithExtension(
provider.id,
query,
options: {'limit': 5, 'filter': ?trackFilter},
);
}
return await PlatformBridge.searchTracksWithMetadataProvider(
provider.id,
query,
limit: 5,
);
} catch (error) {
if (!allowVerificationRetry || !isExtensionVerificationRequired(error)) {
rethrow;
}
final extensionId = usesAutomaticProvider
? extensionIdFromVerificationError(
error,
extensionState.extensions.map((extension) => extension.id),
)
: selectedProvider?.id;
if (extensionId == null || extensionId.isEmpty) rethrow;
_log.i(
'Metadata autofill requires verification; waiting for $extensionId',
);
final verified = await openVerificationAndAwaitGrant(
extensionId,
browserMode: ProviderScope.containerOf(
context,
listen: false,
).read(settingsProvider).extensionVerificationBrowserMode,
);
if (!verified || !mounted) rethrow;
return _searchAutoFillCandidates(
query: query,
usesAutomaticProvider: usesAutomaticProvider,
selectedProvider: selectedProvider,
extensionState: extensionState,
allowVerificationRetry: false,
);
}
}
Future<void> _fetchAutoFillPreview() async {
if (_autoFillFields.isEmpty) {
_showSheetSnackBar(context.l10n.editMetadataAutoFillNoneSelected);
@@ -1067,28 +1131,12 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
if (needsTrackLookup && best == null) {
final query = queryParts.join(' ');
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, 'filter': ?trackFilter},
);
} else {
results = await PlatformBridge.searchTracksWithMetadataProvider(
selectedProvider.id,
query,
limit: 5,
);
}
final results = await _searchAutoFillCandidates(
query: query,
usesAutomaticProvider: usesAutomaticProvider,
selectedProvider: selectedProvider,
extensionState: extensionState,
);
if (!mounted) return;
+1
View File
@@ -30,6 +30,7 @@ import 'package:spotiflac_android/utils/logger.dart';
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
import 'package:spotiflac_android/utils/mime_utils.dart';
import 'package:spotiflac_android/utils/image_cache_utils.dart';
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
import 'package:spotiflac_android/utils/user_facing_error.dart';
+31
View File
@@ -25,6 +25,37 @@ bool isExtensionVerificationRequired(Object error) {
_containsHttpStatusCode(message, '428');
}
String? extensionIdFromVerificationError(
Object error,
Iterable<String> knownExtensionIds,
) {
final idsByNormalized = <String, String>{
for (final id in knownExtensionIds)
if (id.trim().isNotEmpty) id.trim().toLowerCase(): id.trim(),
};
if (idsByNormalized.isEmpty) return null;
final message = error.toString();
final canonicalMatch = RegExp(
r'''extension\s+['"]([^'"]+)['"]''',
caseSensitive: false,
).firstMatch(message);
final canonicalId = canonicalMatch?.group(1)?.trim().toLowerCase();
if (canonicalId != null && idsByNormalized.containsKey(canonicalId)) {
return idsByNormalized[canonicalId];
}
final lowerMessage = message.toLowerCase();
final orderedIds = idsByNormalized.keys.toList()
..sort((a, b) => b.length.compareTo(a.length));
for (final normalizedId in orderedIds) {
if (lowerMessage.contains(normalizedId)) {
return idsByNormalized[normalizedId];
}
}
return null;
}
bool _containsHttpStatusCode(String message, String code) {
return message.contains('http $code') ||
message.contains('http status $code') ||
+24
View File
@@ -0,0 +1,24 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';
void main() {
test('extracts the extension that raised a verification challenge', () {
expect(
extensionIdFromVerificationError(
"verification_required: extension 'tidal-web' needs verification",
const ['amazon-web', 'tidal-web'],
),
'tidal-web',
);
});
test('prefers the longest known extension id in legacy errors', () {
expect(
extensionIdFromVerificationError(
'qobuz-web verification_required',
const ['qobuz', 'qobuz-web'],
),
'qobuz-web',
);
});
}