From 935e00db5a2fc840b4e8d615162c43be5a5841e7 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 26 Jan 2026 01:31:20 +0100 Subject: [PATCH] fix(downloads): resume after connection loss close #265 --- lib/services/download_manager_service.dart | 46 ++++++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/lib/services/download_manager_service.dart b/lib/services/download_manager_service.dart index 093aa35f..ac489bef 100644 --- a/lib/services/download_manager_service.dart +++ b/lib/services/download_manager_service.dart @@ -18,6 +18,9 @@ import '../utils/codec_utils.dart'; import '../utils/plex_cache_parser.dart'; /// Extension methods on AppDatabase for download operations +/// Result of a download attempt for queue processing decisions +enum _DownloadResult { success, networkError, permanentFailure } + extension DownloadDatabaseOperations on AppDatabase { /// Insert a new download into the database Future insertDownload({ @@ -193,6 +196,23 @@ class DownloadManagerService { return shouldBlockDownloadOnCellular(); } + /// Determine if an error is a retriable network error vs a permanent failure + bool _isRetriableNetworkError(Object e) { + if (e is DioException) { + switch (e.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.receiveTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.connectionError: + return true; + default: + return false; + } + } + if (e is SocketException) return true; + return false; + } + /// Public method to check if downloads should be blocked due to cellular-only setting /// Can be used by DownloadProvider to show user-friendly error static Future shouldBlockDownloadOnCellular() async { @@ -295,7 +315,13 @@ class DownloadManagerService { break; } - await _startDownload(nextItem.mediaGlobalKey, client, nextItem); + final result = await _startDownload(nextItem.mediaGlobalKey, client, nextItem); + + // If download failed due to network, wait before retrying to avoid rapid loops + if (result == _DownloadResult.networkError) { + appLogger.i('Network error - waiting 5 seconds before processing next item'); + await Future.delayed(const Duration(seconds: 5)); + } } } finally { _isProcessingQueue = false; @@ -318,7 +344,8 @@ class DownloadManagerService { } /// Start downloading a specific item - Future _startDownload(String globalKey, PlexClient client, DownloadQueueItem queueItem) async { + /// Returns the result of the download attempt for queue processing decisions + Future<_DownloadResult> _startDownload(String globalKey, PlexClient client, DownloadQueueItem queueItem) async { try { appLogger.i('Starting download for $globalKey'); @@ -473,6 +500,7 @@ class DownloadManagerService { _activeDownloads.remove(globalKey); appLogger.i('Download completed for $globalKey'); + return _DownloadResult.success; } catch (e) { // Check if this was a user-initiated cancel/pause (not a real failure) if (e is DioException && e.type == DioExceptionType.cancel) { @@ -480,15 +508,25 @@ class DownloadManagerService { // Just clean up and exit without marking as failed appLogger.d('Download cancelled/paused for $globalKey: ${e.message}'); _activeDownloads.remove(globalKey); - return; + return _DownloadResult.success; // User action, not a failure } + // Check if this is a retriable network error + if (_isRetriableNetworkError(e)) { + // Network error - keep in queue for auto-retry when connectivity returns + appLogger.w('Download interrupted by network error for $globalKey, will retry on reconnect'); + await _transitionStatus(globalKey, DownloadStatus.queued); + _activeDownloads.remove(globalKey); + return _DownloadResult.networkError; // Stay in queue - will auto-retry + } + + // Permanent failure - remove from queue appLogger.e('Download failed for $globalKey', error: e); await _transitionStatus(globalKey, DownloadStatus.failed, errorMessage: e.toString()); await _database.updateDownloadError(globalKey, e.toString()); - // Remove from queue to prevent endless retry loop await _database.removeFromQueue(globalKey); _activeDownloads.remove(globalKey); + return _DownloadResult.permanentFailure; } }