From 55be8fc8274af1a386ff425f962270b5f6f58242 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 27 Jul 2026 01:43:56 +0700 Subject: [PATCH] fix(download): restore fast SQL-only duplicate check so huge playlists queue again --- lib/providers/download_history_provider.dart | 30 +++++++------------- lib/widgets/track_detail_actions.dart | 22 +++++++++----- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/lib/providers/download_history_provider.dart b/lib/providers/download_history_provider.dart index 6fbfa147..5e8806d3 100644 --- a/lib/providers/download_history_provider.dart +++ b/lib/providers/download_history_provider.dart @@ -1063,34 +1063,24 @@ final downloadHistoryExistsProvider = FutureProvider.autoDispose ); }); +// Deliberately no per-row verifyOrRepairHistoryItem here (issue #495): on a +// >500-track playlist that verify pass meant one SAF stat round-trip per +// already-downloaded track, and any loadedIndexVersion bump mid-pass restarted +// it from zero — above ~500 tracks the future never settled and "Download all" +// silently did nothing. Stale rows are reconciled by the startup repair and +// orphan-cleanup passes; the single-track provider above keeps the verify. final downloadHistoryBatchExistsProvider = FutureProvider.autoDispose .family, HistoryBatchLookupRequest>((ref, request) async { ref.watch( downloadHistoryProvider.select((state) => state.loadedIndexVersion), ); - final notifier = ref.read(downloadHistoryProvider.notifier); final rows = await HistoryDatabase.instance.findExistingTracks( request.tracks, ); - final found = {}; - const chunkSize = 16; - for (var start = 0; start < rows.length; start += chunkSize) { - final end = min(start + chunkSize, rows.length); - final checks = await Future.wait( - List.generate(end - start, (offset) async { - final index = start + offset; - final row = rows[index]; - if (row == null) return null; - final exists = await notifier.verifyOrRepairHistoryItem( - DownloadHistoryItem.fromJson(row), - ); - if (!exists) return null; - return request.tracks[index].lookupKey; - }), - ); - found.addAll(checks.whereType()); - } - return found; + return { + for (var i = 0; i < rows.length; i++) + if (rows[i] != null) request.tracks[i].lookupKey, + }; }); class DownloadedAlbumTracksRequest { diff --git a/lib/widgets/track_detail_actions.dart b/lib/widgets/track_detail_actions.dart index 0fd225d5..d5b59178 100644 --- a/lib/widgets/track_detail_actions.dart +++ b/lib/widgets/track_detail_actions.dart @@ -7,6 +7,7 @@ import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/library_collections_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/utils/logger.dart'; import 'package:spotiflac_android/widgets/download_service_picker.dart'; import 'package:spotiflac_android/widgets/view_queue_snackbar_action.dart'; @@ -142,13 +143,20 @@ Future queueTracksSkippingDownloaded( final historyLookups = tracks .map(historyLookupForTrack) .toList(growable: false); - final existingHistoryKeys = skipExisting - ? await ref.read( - downloadHistoryBatchExistsProvider( - HistoryBatchLookupRequest(historyLookups), - ).future, - ) - : const {}; + Set existingHistoryKeys = const {}; + if (skipExisting) { + try { + existingHistoryKeys = await ref.read( + downloadHistoryBatchExistsProvider( + HistoryBatchLookupRequest(historyLookups), + ).future, + ); + } catch (e) { + // Queueing without the skip beats silently doing nothing: several + // callers fire this flow unawaited, so a throw here would vanish. + AppLogger('DownloadFlow').w('Duplicate check failed, queueing all: $e'); + } + } if (!context.mounted) return; final localLibState = (skipExisting &&