From 66a89d9e8e988afee444610dd9003c8e0e7a68f8 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 17 Mar 2026 15:26:51 +0700 Subject: [PATCH] fix: properly stop active downloads when pausing the queue --- lib/providers/download_queue_provider.dart | 166 +++++++++++++++++++-- 1 file changed, 156 insertions(+), 10 deletions(-) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 8c2bb5f6..5898445c 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1005,6 +1005,7 @@ class DownloadQueueNotifier extends Notifier { int _lastNotifPercent = -1; int _lastNotifQueueCount = -1; final Set _locallyCancelledItemIds = {}; + final Set _pausePendingItemIds = {}; double _normalizeProgressForUi(double value) { final clamped = value.clamp(0.0, 1.0).toDouble(); @@ -1324,6 +1325,10 @@ class DownloadQueueNotifier extends Notifier { if (localItem == null) { continue; } + if (_isPausePending(itemId)) { + PlatformBridge.clearItemProgress(itemId).catchError((_) {}); + continue; + } if (localItem.status == DownloadStatus.skipped) { PlatformBridge.clearItemProgress(itemId).catchError((_) {}); continue; @@ -2123,12 +2128,42 @@ class DownloadQueueNotifier extends Notifier { return resolved?.status == DownloadStatus.skipped; } + bool _isPausePending(String id) => _pausePendingItemIds.contains(id); + + void _requeueItemForPause(String id) { + final updatedItems = state.items + .map((item) { + if (item.id != id) return item; + if (item.status == DownloadStatus.completed || + item.status == DownloadStatus.failed || + item.status == DownloadStatus.skipped) { + return item; + } + return item.copyWith( + status: DownloadStatus.queued, + progress: 0, + speedMBps: 0, + bytesReceived: 0, + ); + }) + .toList(growable: false); + + final currentDownload = state.currentDownload?.id == id + ? null + : state.currentDownload; + state = state.copyWith( + items: updatedItems, + currentDownload: currentDownload, + ); + } + void _requestNativeCancel(String id) { PlatformBridge.cancelDownload(id).catchError((_) {}); PlatformBridge.clearItemProgress(id).catchError((_) {}); } void cancelItem(String id) { + _pausePendingItemIds.remove(id); _locallyCancelledItemIds.add(id); updateItemStatus(id, DownloadStatus.skipped); _requestNativeCancel(id); @@ -2161,6 +2196,7 @@ class DownloadQueueNotifier extends Notifier { .toList(growable: false); if (activeIds.isNotEmpty) { + _pausePendingItemIds.addAll(activeIds); _locallyCancelledItemIds.addAll(activeIds); for (final id in activeIds) { _requestNativeCancel(id); @@ -2173,11 +2209,29 @@ class DownloadQueueNotifier extends Notifier { if (!wasProcessing) { _locallyCancelledItemIds.clear(); } + _pausePendingItemIds.clear(); } void pauseQueue() { if (state.isProcessing && !state.isPaused) { - state = state.copyWith(isPaused: true); + final activeIds = state.items + .where( + (item) => + item.status == DownloadStatus.downloading || + item.status == DownloadStatus.finalizing, + ) + .map((item) => item.id) + .toSet(); + + if (activeIds.isNotEmpty) { + _pausePendingItemIds.addAll(activeIds); + for (final id in activeIds) { + _requestNativeCancel(id); + _requeueItemForPause(id); + } + } + + state = state.copyWith(isPaused: true, currentDownload: null); _notificationService.cancelDownloadNotification(); _log.i('Queue paused'); } @@ -3268,7 +3322,11 @@ class DownloadQueueNotifier extends Notifier { } final queuedItems = state.items - .where((item) => item.status == DownloadStatus.queued) + .where( + (item) => + item.status == DownloadStatus.queued && + !_pausePendingItemIds.contains(item.id), + ) .toList(); if (queuedItems.isEmpty && activeDownloads.isEmpty) { @@ -3313,11 +3371,13 @@ class DownloadQueueNotifier extends Notifier { _stopProgressPolling(); final remainingIds = state.items.map((item) => item.id).toSet(); _locallyCancelledItemIds.removeWhere((id) => !remainingIds.contains(id)); + _pausePendingItemIds.removeWhere((id) => !remainingIds.contains(id)); } Future _downloadSingleItem(DownloadItem item) async { _log.d('Processing: ${item.track.name} by ${item.track.artistName}'); _log.d('Cover URL: ${item.track.coverUrl}'); + var pausedDuringThisRun = false; final currentItem = _findItemById(item.id) ?? item; if (_isLocallyCancelled(item.id, item: currentItem)) { @@ -3325,11 +3385,33 @@ class DownloadQueueNotifier extends Notifier { return; } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + _requeueItemForPause(item.id); + _log.i('Download is pause-pending before start, skipping'); + return; + } + state = state.copyWith(currentDownload: item); updateItemStatus(item.id, DownloadStatus.downloading); try { + bool shouldAbortWork(String stage) { + final current = _findItemById(item.id); + if (_isLocallyCancelled(item.id, item: current)) { + _log.i('Download was cancelled $stage, skipping'); + return true; + } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + _requeueItemForPause(item.id); + _log.i('Download pause requested $stage, re-queueing'); + return true; + } + return false; + } + final settings = ref.read(settingsProvider); final metadataEmbeddingEnabled = settings.embedMetadata; @@ -3410,6 +3492,10 @@ class DownloadQueueNotifier extends Notifier { _log.w('Failed to enrich metadata: $e'); _log.w('Stack trace: $stack'); } + + if (shouldAbortWork('during metadata enrichment')) { + return; + } } _log.d('Track coverUrl after enrichment: ${trackToDownload.coverUrl}'); @@ -3523,6 +3609,10 @@ class DownloadQueueNotifier extends Notifier { } catch (e) { _log.w('Failed to search Deezer by ISRC: $e'); } + + if (shouldAbortWork('during Deezer ISRC lookup')) { + return; + } } // Fallback: Use SongLink to convert Spotify ID to Deezer ID @@ -3623,6 +3713,10 @@ class DownloadQueueNotifier extends Notifier { } catch (e) { _log.w('Failed to convert Spotify to Deezer via SongLink: $e'); } + + if (shouldAbortWork('during SongLink availability lookup')) { + return; + } } if (deezerTrackId != null && deezerTrackId.isNotEmpty) { @@ -3642,6 +3736,10 @@ class DownloadQueueNotifier extends Notifier { } catch (e) { _log.w('Failed to fetch extended metadata from Deezer: $e'); } + + if (shouldAbortWork('during extended metadata lookup')) { + return; + } } Map result; @@ -3760,8 +3858,7 @@ class DownloadQueueNotifier extends Notifier { ); } - if (_isLocallyCancelled(item.id)) { - _log.i('Download was cancelled before native download start, skipping'); + if (shouldAbortWork('before native download start')) { return; } @@ -3803,10 +3900,8 @@ class DownloadQueueNotifier extends Notifier { _log.d('Result: $result'); final itemAfterResult = _findItemById(item.id); - final cancelledAfterResult = - itemAfterResult == null || - _isLocallyCancelled(item.id, item: itemAfterResult); - if (cancelledAfterResult) { + if (itemAfterResult == null || + _isLocallyCancelled(item.id, item: itemAfterResult)) { _log.i('Download was cancelled, skipping result processing'); final filePath = result['file_path'] as String?; if (filePath != null && result['success'] == true) { @@ -3816,6 +3911,18 @@ class DownloadQueueNotifier extends Notifier { return; } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + final filePath = result['file_path'] as String?; + if (filePath != null && result['success'] == true) { + await deleteFile(filePath); + _log.d('Deleted paused download file: $filePath'); + } + _requeueItemForPause(item.id); + _log.i('Download pause requested after result, re-queueing'); + return; + } + if (result['success'] == true) { var filePath = result['file_path'] as String?; final reportedFileName = result['file_name'] as String?; @@ -4349,6 +4456,19 @@ class DownloadQueueNotifier extends Notifier { return; } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + if (filePath != null) { + await deleteFile(filePath); + _log.d( + 'Deleted paused download file during finalization: $filePath', + ); + } + _requeueItemForPause(item.id); + _log.i('Download pause requested during finalization, re-queueing'); + return; + } + // SAF downloads should end with content URI. If we still have a // transient FD path, recover URI from SAF metadata to keep history // dedup/exclusion stable. @@ -4616,11 +4736,26 @@ class DownloadQueueNotifier extends Notifier { return; } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + _requeueItemForPause(item.id); + _log.i('Download pause requested after backend failure, re-queueing'); + return; + } + final errorMsg = result['error'] as String? ?? 'Download failed'; final errorTypeStr = result['error_type'] as String? ?? 'unknown'; if (errorTypeStr == 'cancelled') { - _log.i('Download was cancelled by backend, skipping error handling'); - updateItemStatus(item.id, DownloadStatus.skipped); + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + _requeueItemForPause(item.id); + _log.i('Download was paused by backend cancellation, re-queueing'); + } else { + _log.i( + 'Download was cancelled by backend, skipping error handling', + ); + updateItemStatus(item.id, DownloadStatus.skipped); + } return; } @@ -4679,6 +4814,13 @@ class DownloadQueueNotifier extends Notifier { return; } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + _requeueItemForPause(item.id); + _log.i('Download pause requested after exception, re-queueing'); + return; + } + _log.e('Exception: $e', e, stackTrace); String errorMsg = e.toString(); @@ -4704,6 +4846,10 @@ class DownloadQueueNotifier extends Notifier { } catch (cleanupErr) { _log.e('Post-exception connection cleanup failed: $cleanupErr'); } + } finally { + if (pausedDuringThisRun) { + _pausePendingItemIds.remove(item.id); + } } } }