feat: extension runtime and provider improvements

This commit is contained in:
zarzet
2026-06-26 04:14:51 +07:00
parent 21347420f3
commit ee35f52baf
17 changed files with 1019 additions and 56 deletions
+25 -3
View File
@@ -22,6 +22,7 @@ import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
import 'package:spotiflac_android/utils/artist_utils.dart';
import 'package:spotiflac_android/utils/int_utils.dart';
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';
export 'package:spotiflac_android/services/history_database.dart'
show HistoryLookupRequest, HistoryBatchLookupRequest;
@@ -4902,7 +4903,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
'totalDiscs': track.totalDiscs!.toString(),
if (track.isrc != null) 'isrc': track.isrc!,
if (label != null && label.isNotEmpty) 'label': label,
if (copyright != null && copyright.isNotEmpty) 'copyright': copyright,
if (copyright != null && copyright.isNotEmpty)
'copyright': copyright,
if (shouldEmbedLyrics) 'lyrics': ?lrcContent,
};
final ac4Result = await PlatformBridge.writeAC4Metadata(
@@ -6636,6 +6638,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
return DownloadErrorType.network;
case 'permission':
return DownloadErrorType.permission;
case 'verification_required':
return DownloadErrorType.verificationRequired;
default:
return DownloadErrorType.unknown;
}
@@ -6643,6 +6647,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
DownloadErrorType _downloadErrorTypeFromMessage(String errorMsg) {
final lowerMsg = errorMsg.toLowerCase();
if (isExtensionVerificationRequired(errorMsg)) {
return DownloadErrorType.verificationRequired;
}
if (errorMsg.contains('429') ||
lowerMsg.contains('rate limit') ||
lowerMsg.contains('too many requests')) {
@@ -7609,7 +7616,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
// Repair AC-4 (dac4 + ISO MP4) using the still-present encrypted
// source. No-op for other codecs.
try {
await PlatformBridge.ensureAC4Config(decryptedTempPath, tempPath);
await PlatformBridge.ensureAC4Config(
decryptedTempPath,
tempPath,
);
} catch (e) {
_log.w('AC-4 container repair skipped: $e');
}
@@ -7688,7 +7698,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
// Repair AC-4 (dac4 + ISO MP4) using the still-present encrypted
// source before discarding it. No-op for other codecs.
try {
await PlatformBridge.ensureAC4Config(decryptedPath, encryptedSource);
await PlatformBridge.ensureAC4Config(
decryptedPath,
encryptedSource,
);
} catch (e) {
_log.w('AC-4 container repair skipped: $e');
}
@@ -8862,6 +8875,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
case 'permission':
errorType = DownloadErrorType.permission;
break;
case 'verification_required':
errorType = DownloadErrorType.verificationRequired;
break;
default:
errorType = _downloadErrorTypeFromMessage(errorMsg);
}
@@ -8873,6 +8889,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
error: errorMsg,
errorType: errorType,
);
if (errorType == DownloadErrorType.verificationRequired) {
unawaited(openPendingExtensionVerification(item.service));
}
_failedInSession++;
try {
@@ -8927,6 +8946,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
error: errorMsg,
errorType: errorType,
);
if (errorType == DownloadErrorType.verificationRequired) {
unawaited(openPendingExtensionVerification(item.service));
}
_failedInSession++;
try {
+88 -5
View File
@@ -1,8 +1,11 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/logger.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
@@ -195,9 +198,20 @@ class SearchPlaylist {
class TrackNotifier extends Notifier<TrackState> {
int _currentRequestId = 0;
StreamSubscription<ExtensionSessionGrantEvent>? _sessionGrantSub;
_PendingVerificationSearch? _pendingVerificationSearch;
bool _retryingPendingVerificationSearch = false;
@override
TrackState build() {
_sessionGrantSub ??= PlatformBridge.extensionSessionGrantEvents().listen(
_handleExtensionSessionGrantCompleted,
);
ref.onDispose(() {
_sessionGrantSub?.cancel();
_sessionGrantSub = null;
_pendingVerificationSearch = null;
});
return const TrackState();
}
@@ -314,7 +328,8 @@ class TrackNotifier extends Notifier<TrackState> {
.map((a) => _parseArtistAlbum(a as Map<String, dynamic>))
.toList();
final topTracksList = artistData['top_tracks'] as List<dynamic>? ?? [];
final topTracksList =
artistData['top_tracks'] as List<dynamic>? ?? [];
final topTracks = topTracksList
.map(
(t) => _parseSearchTrack(
@@ -359,10 +374,7 @@ class TrackNotifier extends Notifier<TrackState> {
}
}
Future<void> search(
String query, {
String? filterOverride,
}) async {
Future<void> search(String query, {String? filterOverride}) async {
final requestId = ++_currentRequestId;
final currentFilter = filterOverride ?? state.selectedSearchFilter;
final requestFilter = currentFilter == 'all' ? null : currentFilter;
@@ -601,6 +613,7 @@ class TrackNotifier extends Notifier<TrackState> {
_log.i(
'Custom search complete: ${tracks.length} tracks parsed (source=$extensionId)',
);
_clearPendingVerificationSearch(extensionId, query, currentFilter);
state = TrackState(
tracks: tracks,
@@ -614,6 +627,18 @@ class TrackNotifier extends Notifier<TrackState> {
} catch (e, stackTrace) {
if (!_isRequestValid(requestId)) return;
_log.e('Custom search failed: $e', e, stackTrace);
if (isExtensionVerificationRequired(e)) {
_pendingVerificationSearch = _PendingVerificationSearch(
extensionId: extensionId,
query: query,
options: Map<String, dynamic>.from(
options ?? const <String, dynamic>{},
),
selectedFilter: currentFilter,
createdAt: DateTime.now(),
);
await openPendingExtensionVerification(extensionId);
}
state = TrackState(
isLoading: false,
error: e.toString(),
@@ -624,6 +649,49 @@ class TrackNotifier extends Notifier<TrackState> {
}
}
void _clearPendingVerificationSearch(
String extensionId,
String query,
String? selectedFilter,
) {
final pending = _pendingVerificationSearch;
if (pending == null) return;
if (pending.extensionId == extensionId &&
pending.query == query &&
pending.selectedFilter == selectedFilter) {
_pendingVerificationSearch = null;
}
}
void _handleExtensionSessionGrantCompleted(ExtensionSessionGrantEvent event) {
if (!event.success || _retryingPendingVerificationSearch) return;
final pending = _pendingVerificationSearch;
if (pending == null || pending.extensionId != event.extensionId) return;
if (DateTime.now().difference(pending.createdAt) >
const Duration(minutes: 10)) {
_pendingVerificationSearch = null;
return;
}
_pendingVerificationSearch = null;
_retryingPendingVerificationSearch = true;
Future<void>.delayed(const Duration(milliseconds: 300), () async {
try {
_log.i(
'Retrying custom search after verification: extension=${pending.extensionId}',
);
await customSearch(
pending.extensionId,
pending.query,
options: pending.options,
selectedFilter: pending.selectedFilter,
);
} finally {
_retryingPendingVerificationSearch = false;
}
});
}
Future<void> checkAvailability(int index) async {
if (index < 0 || index >= state.tracks.length) return;
@@ -826,7 +894,22 @@ class TrackNotifier extends Notifier<TrackState> {
totalTracks: data['total_tracks'] as int? ?? 0,
);
}
}
class _PendingVerificationSearch {
final String extensionId;
final String query;
final Map<String, dynamic> options;
final String? selectedFilter;
final DateTime createdAt;
const _PendingVerificationSearch({
required this.extensionId,
required this.query,
required this.options,
required this.selectedFilter,
required this.createdAt,
});
}
final trackProvider = NotifierProvider<TrackNotifier, TrackState>(