mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-17 16:37:17 +02:00
feat: extension runtime and provider improvements
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>(
|
||||
|
||||
Reference in New Issue
Block a user