mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 04:42:02 +02:00
fix(library): index native completions and refresh background downloads
This commit is contained in:
@@ -394,6 +394,13 @@ class _EagerInitializationState extends ConsumerState<EagerInitialization>
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
unawaited(_consumeVerificationNotification());
|
||||
CoverCacheManager.scheduleMaintenance();
|
||||
// Native downloads can finish while Flutter is suspended. Refresh the
|
||||
// persisted history even when no queued item remains to reconcile.
|
||||
if (ref.exists(downloadHistoryProvider)) {
|
||||
unawaited(
|
||||
ref.read(downloadHistoryProvider.notifier).reloadFromStorage(),
|
||||
);
|
||||
}
|
||||
_maybeAutoScanLocalLibrary();
|
||||
if (ref.exists(localLibraryProvider)) {
|
||||
unawaited(
|
||||
|
||||
@@ -82,6 +82,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
bool _isAudioMetadataBackfillInProgress = false;
|
||||
bool _startupMaintenanceScheduled = false;
|
||||
Future<void> _historyWriteChain = Future<void>.value();
|
||||
Future<void>? _reloadFuture;
|
||||
Timer? _indexBumpTimer;
|
||||
DateTime? _lastIndexBumpAt;
|
||||
static const _indexBumpWindow = Duration(seconds: 1);
|
||||
@@ -97,7 +98,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
if (_isLoaded) return;
|
||||
_isLoaded = true;
|
||||
Future.microtask(() async {
|
||||
await _loadFromDatabase();
|
||||
await reloadFromStorage();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,9 +138,10 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadFromStorage() async {
|
||||
await _loadFromDatabase();
|
||||
}
|
||||
Future<void> reloadFromStorage() => _reloadFuture ??=
|
||||
_enqueueHistoryWrite(_loadFromDatabase).whenComplete(() {
|
||||
_reloadFuture = null;
|
||||
});
|
||||
|
||||
void _bumpHistoryRevision() {
|
||||
state = state.copyWith(loadedIndexVersion: state.loadedIndexVersion + 1);
|
||||
|
||||
@@ -43,6 +43,7 @@ import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
|
||||
import 'package:spotiflac_android/utils/progress_stream_poller.dart';
|
||||
|
||||
import 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
import 'package:spotiflac_android/services/native_download_history.dart';
|
||||
|
||||
export 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
export 'package:spotiflac_android/providers/download_queue_state.dart';
|
||||
|
||||
@@ -1261,40 +1261,20 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
await persistBeforePublishingDownloadCompletion(
|
||||
persist: () async {
|
||||
if (!settings.saveDownloadHistory) return;
|
||||
final historyItem = result['history_item'];
|
||||
if (historyItem is Map) {
|
||||
try {
|
||||
await ref
|
||||
.read(downloadHistoryProvider.notifier)
|
||||
.adoptNativeHistoryItem(
|
||||
DownloadHistoryItem.fromJson(
|
||||
Map<String, dynamic>.from(historyItem),
|
||||
),
|
||||
preserveTrackVariant: item.preserveQualityVariant,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.w('Failed to adopt native history item: $e');
|
||||
await _persistNativeFinalizedHistoryFallback(
|
||||
context,
|
||||
result,
|
||||
nativeFinalizedFilePath,
|
||||
);
|
||||
}
|
||||
} else if (result['history_written'] == true ||
|
||||
result['already_exists'] == true) {
|
||||
await ref
|
||||
.read(downloadHistoryProvider.notifier)
|
||||
.reloadFromStorage();
|
||||
} else {
|
||||
_log.w(
|
||||
'Native finalizer completed without history; persisting Dart fallback',
|
||||
);
|
||||
await _persistNativeFinalizedHistoryFallback(
|
||||
final history = ref.read(downloadHistoryProvider.notifier);
|
||||
await reconcileNativeDownloadHistory(
|
||||
result: result,
|
||||
adopt: (json) => history.adoptNativeHistoryItem(
|
||||
DownloadHistoryItem.fromJson(json),
|
||||
preserveTrackVariant: item.preserveQualityVariant,
|
||||
),
|
||||
reload: history.reloadFromStorage,
|
||||
persistFallback: () => _persistNativeFinalizedHistoryFallback(
|
||||
context,
|
||||
result,
|
||||
nativeFinalizedFilePath,
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
},
|
||||
publish: () {
|
||||
_completedInSession++;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
/// A completed file and a persisted Library entry are separate guarantees.
|
||||
/// In particular, `already_exists` does not mean history was ever saved.
|
||||
Future<void> reconcileNativeDownloadHistory({
|
||||
required Map<String, dynamic> result,
|
||||
required Future<void> Function(Map<String, dynamic>) adopt,
|
||||
required Future<void> Function() reload,
|
||||
required Future<void> Function() persistFallback,
|
||||
}) async {
|
||||
final historyItem = result['history_item'];
|
||||
if (historyItem is Map) {
|
||||
try {
|
||||
await adopt(Map<String, dynamic>.from(historyItem));
|
||||
return;
|
||||
} catch (error) {
|
||||
AppLogger('DownloadHistory').w(
|
||||
'Failed to adopt native history item, retrying from download result: $error',
|
||||
);
|
||||
}
|
||||
} else if (result['history_written'] == true) {
|
||||
await reload();
|
||||
return;
|
||||
}
|
||||
await persistFallback();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/services/native_download_history.dart';
|
||||
|
||||
void main() {
|
||||
for (final alreadyExists in [false, true]) {
|
||||
test(
|
||||
'unindexed completed file is persisted before publication (exists: $alreadyExists)',
|
||||
() async {
|
||||
final saved = Completer<void>();
|
||||
final events = <String>[];
|
||||
final completion = persistBeforePublishingDownloadCompletion(
|
||||
persist: () => reconcileNativeDownloadHistory(
|
||||
result: {'already_exists': alreadyExists, 'history_written': false},
|
||||
adopt: (_) async => fail('No history item was supplied'),
|
||||
reload: () async => fail('Reload cannot index an unpersisted file'),
|
||||
persistFallback: () async {
|
||||
events.add('persist');
|
||||
await saved.future;
|
||||
},
|
||||
),
|
||||
publish: () => events.add('publish'),
|
||||
);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(events, ['persist']);
|
||||
saved.complete();
|
||||
await completion;
|
||||
expect(events, ['persist', 'publish']);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
test(
|
||||
'native history is adopted without a second fallback insertion',
|
||||
() async {
|
||||
await reconcileNativeDownloadHistory(
|
||||
result: {
|
||||
'history_item': {'id': 'native-row'},
|
||||
'history_written': true,
|
||||
},
|
||||
adopt: (json) async => expect(json['id'], 'native-row'),
|
||||
reload: () async => fail('Native row should be adopted directly'),
|
||||
persistFallback: () async => fail('No duplicate fallback'),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('confirmed native write reloads the Library', () async {
|
||||
var refreshed = false;
|
||||
await reconcileNativeDownloadHistory(
|
||||
result: {'history_written': true},
|
||||
adopt: (_) async => fail('No row supplied'),
|
||||
reload: () async => refreshed = true,
|
||||
persistFallback: () async => fail('Native history already persisted'),
|
||||
);
|
||||
expect(refreshed, isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'bad native history falls back and persistence errors prevent completion',
|
||||
() async {
|
||||
var published = false;
|
||||
await expectLater(
|
||||
persistBeforePublishingDownloadCompletion(
|
||||
persist: () => reconcileNativeDownloadHistory(
|
||||
result: {'history_item': <String, dynamic>{}},
|
||||
adopt: (_) async => throw const FormatException('incomplete row'),
|
||||
reload: () async => fail('Unconfirmed history write'),
|
||||
persistFallback: () async =>
|
||||
throw StateError('storage unavailable'),
|
||||
),
|
||||
publish: () => published = true,
|
||||
),
|
||||
throwsStateError,
|
||||
);
|
||||
expect(published, isFalse);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user