diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt index a504fcdf..3f037a96 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt @@ -564,12 +564,12 @@ class DownloadService : Service() { nativeWorkerCurrentItemId = request.itemId currentTrackName = request.trackName currentArtistName = request.artistName - currentStatus = "downloading" + currentStatus = "preparing" lastProgress = 0L lastTotal = 0L updateNotification(0, 0) updateNativeWorkerItem(request.itemId) { - it.status = "downloading" + it.status = "preparing" it.progress = 0.0 it.bytesReceived = 0L it.bytesTotal = 0L @@ -580,7 +580,7 @@ class DownloadService : Service() { isRunning = true, isPaused = false, currentItemId = request.itemId, - message = "Downloading", + message = "Preparing", settingsJson = settingsJson, includeItems = true ) @@ -1100,14 +1100,34 @@ class DownloadService : Service() { val root = JSONObject(raw) val items = root.optJSONObject("items") ?: return val progress = items.optJSONObject(itemId) ?: return + val backendStatus = progress.optString("status", "downloading") val bytesReceived = progress.optLong("bytes_received", 0L) val bytesTotal = progress.optLong("bytes_total", 0L) + if (backendStatus == "preparing") { + currentStatus = "preparing" + updateNativeWorkerItem(itemId) { + it.status = "preparing" + it.progress = 0.0 + it.bytesReceived = 0L + it.bytesTotal = 0L + } + lastProgress = 0L + lastTotal = 0L + updateNotification(0L, 0L) + return + } val progressValue = if (bytesTotal > 0L) { bytesReceived.toDouble() / bytesTotal.toDouble() } else { progress.optDouble("progress", 0.0) }.coerceIn(0.0, 1.0) + currentStatus = if (backendStatus == "finalizing") { + "finalizing" + } else { + "downloading" + } updateNativeWorkerItem(itemId) { + it.status = currentStatus it.progress = progressValue it.bytesReceived = bytesReceived it.bytesTotal = bytesTotal diff --git a/go_backend/progress.go b/go_backend/progress.go index 5837dfb4..f527f1aa 100644 --- a/go_backend/progress.go +++ b/go_backend/progress.go @@ -213,8 +213,8 @@ func StartItemProgress(itemID string) { BytesTotal: 0, BytesReceived: 0, Progress: 0, - IsDownloading: true, - Status: itemProgressStatusDownloading, + IsDownloading: false, + Status: itemProgressStatusPreparing, revision: nextMultiProgressSeqLocked(), } delete(removedProgressSeq, itemID) @@ -316,14 +316,19 @@ func SetItemProgress(itemID string, progress float64, bytesReceived, bytesTotal if item, ok := multiProgress.Items[itemID]; ok { before := itemProgressBridgeState(item) - item.Progress = progress + hasByteProgress := bytesReceived > 0 || bytesTotal > 0 + if item.Status != itemProgressStatusPreparing || hasByteProgress || progress >= 1 { + item.Progress = progress + } else { + item.Progress = 0 + } if bytesReceived > 0 { item.BytesReceived = bytesReceived } if bytesTotal > 0 { item.BytesTotal = bytesTotal } - if progress > 0 || bytesReceived > 0 || bytesTotal > 0 { + if hasByteProgress || progress >= 1 || item.Status == itemProgressStatusDownloading { item.IsDownloading = true item.Status = itemProgressStatusDownloading } diff --git a/go_backend/progress_test.go b/go_backend/progress_test.go index 7390107b..214509aa 100644 --- a/go_backend/progress_test.go +++ b/go_backend/progress_test.go @@ -24,11 +24,13 @@ func TestItemProgressPreparingAndDownloadingStatuses(t *testing.T) { } } - SetItemProgress(itemID, 0.37, 0, 0) + SetItemProgress(itemID, 0.05, 0, 0) if item := multiProgress.Items[itemID]; item == nil { t.Fatal("expected item progress entry to exist after update") - } else if item.Status != itemProgressStatusDownloading { - t.Fatalf("status after progress update = %q, want %q", item.Status, itemProgressStatusDownloading) + } else if item.Status != itemProgressStatusPreparing { + t.Fatalf("status after synthetic pre-download progress = %q, want %q", item.Status, itemProgressStatusPreparing) + } else if item.Progress != 0 { + t.Fatalf("progress after synthetic pre-download progress = %v, want 0", item.Progress) } SetItemDownloading(itemID) @@ -37,6 +39,15 @@ func TestItemProgressPreparingAndDownloadingStatuses(t *testing.T) { } else if item.Status != itemProgressStatusDownloading { t.Fatalf("status after download start = %q, want %q", item.Status, itemProgressStatusDownloading) } + + SetItemProgress(itemID, 0.37, 0, 0) + if item := multiProgress.Items[itemID]; item == nil { + t.Fatal("expected item progress entry to exist after real update") + } else if item.Status != itemProgressStatusDownloading { + t.Fatalf("status after real progress update = %q, want %q", item.Status, itemProgressStatusDownloading) + } else if item.Progress != 0.37 { + t.Fatalf("progress after real update = %v, want 0.37", item.Progress) + } } func TestItemProgressFinalizingAndCompletedStatuses(t *testing.T) { diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 08a522a8..02381976 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -2099,7 +2099,8 @@ class DownloadQueueNotifier extends Notifier { final progressFromBackend = (itemProgress['progress'] as num?)?.toDouble() ?? 0.0; final hasRealProgress = - bytesReceived > 0 || bytesTotal > 0 || progressFromBackend > 0; + status != 'preparing' && + (bytesReceived > 0 || bytesTotal > 0 || progressFromBackend > 0); if (status == 'finalizing') { progressUpdates[itemId] = const _ProgressUpdate( @@ -2112,7 +2113,7 @@ class DownloadQueueNotifier extends Notifier { continue; } - if (status == 'preparing' && !hasRealProgress) { + if (status == 'preparing') { progressUpdates[itemId] = const _ProgressUpdate( status: DownloadStatus.downloading, progress: 0.0, @@ -2264,10 +2265,7 @@ class DownloadQueueNotifier extends Notifier { final progressPercent = (selectedProgress['progress'] as num?)?.toDouble() ?? 0.0; - final hasRealProgress = - bytesReceived > 0 || bytesTotal > 0 || progressPercent > 0; - - if (backendStatus == 'preparing' && !hasRealProgress) { + if (backendStatus == 'preparing') { notifProgress = 0; notifTotal = 0; } else if (bytesTotal <= 0) { @@ -5400,6 +5398,11 @@ class DownloadQueueNotifier extends Notifier { continue; } + if (status == 'preparing') { + updateItemStatus(itemId, DownloadStatus.downloading, progress: 0.0); + continue; + } + if (status == 'downloading') { updateItemStatus( itemId, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 15d978b2..d26b17fa 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -150,6 +150,10 @@ class _QueueTabState extends ConsumerState { double? _libraryGridScaleStartExtent; int _libraryPageLimit = _libraryPageSize; bool _libraryPageLoadScheduled = false; + final Map<_QueueLibraryCountsRequest, QueueLibraryCounts> + _queueLibraryCountsCache = {}; + final Map<_QueueLibraryPageRequest, _QueueLibraryPageData> + _queueLibraryPageDataCache = {}; double _effectiveTextScale() { final textScale = MediaQuery.textScalerOf(context).scale(1.0); @@ -249,6 +253,55 @@ class _QueueTabState extends ConsumerState { }); } + QueueLibraryCounts _resolveQueueLibraryCounts( + AsyncValue value, + _QueueLibraryCountsRequest request, + ) { + return value.maybeWhen( + data: (counts) { + _queueLibraryCountsCache[request] = counts; + _trimQueueLibraryCaches(); + return counts; + }, + orElse: () => + _queueLibraryCountsCache[request] ?? + const QueueLibraryCounts( + allTrackCount: 0, + albumCount: 0, + singleTrackCount: 0, + ), + ); + } + + _QueueLibraryPageData _resolveQueueLibraryPageData( + AsyncValue<_QueueLibraryPageData>? value, + _QueueLibraryPageRequest request, + ) { + if (value == null) { + return _queueLibraryPageDataCache[request] ?? + const _QueueLibraryPageData(); + } + return value.maybeWhen( + data: (data) { + _queueLibraryPageDataCache[request] = data; + _trimQueueLibraryCaches(); + return data; + }, + orElse: () => + _queueLibraryPageDataCache[request] ?? const _QueueLibraryPageData(), + ); + } + + void _trimQueueLibraryCaches() { + const maxEntries = 24; + while (_queueLibraryCountsCache.length > maxEntries) { + _queueLibraryCountsCache.remove(_queueLibraryCountsCache.keys.first); + } + while (_queueLibraryPageDataCache.length > maxEntries) { + _queueLibraryPageDataCache.remove(_queueLibraryPageDataCache.keys.first); + } + } + bool _handleLibraryScrollNotification({ required ScrollNotification notification, required String filterMode, @@ -2455,14 +2508,7 @@ class _QueueTabState extends ConsumerState { localLibraryEnabled: localLibraryEnabled, ); final countsValue = ref.watch(_queueLibraryCountsProvider(countsRequest)); - final queueCounts = countsValue.maybeWhen( - data: (counts) => counts, - orElse: () => const QueueLibraryCounts( - allTrackCount: 0, - albumCount: 0, - singleTrackCount: 0, - ), - ); + final queueCounts = _resolveQueueLibraryCounts(countsValue, countsRequest); _QueueLibraryPageRequest pageRequest(String filterMode) => _QueueLibraryPageRequest( @@ -2477,17 +2523,19 @@ class _QueueTabState extends ConsumerState { localLibraryEnabled: localLibraryEnabled, ); + final pageRequests = { + for (final mode in _filterModes) mode: pageRequest(mode), + }; final pageValues = >{ - for (final mode in _filterModes) - mode: ref.watch(_queueLibraryPageProvider(pageRequest(mode))), + for (final entry in pageRequests.entries) + entry.key: ref.watch(_queueLibraryPageProvider(entry.value)), }; _QueueLibraryPageData pageData(String filterMode) => - pageValues[filterMode]?.maybeWhen( - data: (data) => data, - orElse: () => const _QueueLibraryPageData(), - ) ?? - const _QueueLibraryPageData(); + _resolveQueueLibraryPageData( + pageValues[filterMode], + pageRequests[filterMode]!, + ); _FilterContentData getFilterData(String filterMode) { return pageData(filterMode).toFilterContentData(