fix(download): complete quality variants reliably

This commit is contained in:
zarzet
2026-07-27 14:22:24 +07:00
parent 856e2cbb9e
commit 73846c40ca
10 changed files with 163 additions and 22 deletions
+9 -1
View File
@@ -74,6 +74,15 @@ Future<void> persistBeforePublishingDownloadCompletion({
publish();
}
/// Keeps native-worker finalization visually distinct from completion.
/// Download bytes may reach 100% before SAF publishing, metadata persistence,
/// and queue reconciliation have finished.
double nativeWorkerFinalizingProgress(double progress) {
final normalized = progress.clamp(0.0, 1.0).toDouble();
if (normalized <= 0) return 0.95;
return min(normalized, 0.99);
}
final _invalidFolderChars = RegExp(r'[<>:"/\\|?*]');
final _trimDotsAndSpacesRegex = RegExp(r'^[. ]+|[. ]+$');
final _trimUnderscoresAndSpacesRegex = RegExp(r'^[_ ]+|[_ ]+$');
@@ -1488,7 +1497,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
_verificationRetryGuard.retainItems(remainingIds);
_rateLimitRetriedItemIds.removeWhere((id) => !remainingIds.contains(id));
}
}
final downloadQueueProvider =
@@ -803,7 +803,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
updateItemStatus(
itemId,
DownloadStatus.finalizing,
progress: progress <= 0 ? 0.95 : progress,
progress: nativeWorkerFinalizingProgress(progress),
);
continue;
}
@@ -811,12 +811,34 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
if (status == 'completed') {
final result = itemSnapshot['result'];
if (result is Map) {
reconciledIds.add(itemId);
await _completeAndroidNativeWorkerItem(
context,
Map<String, dynamic>.from(result),
settings,
);
try {
await _completeAndroidNativeWorkerItem(
context,
Map<String, dynamic>.from(result),
settings,
);
reconciledIds.add(itemId);
} catch (e, stack) {
// A native output can be complete while Library persistence or
// Dart-side adoption fails. Do not leave the queue permanently at
// 100%: surface the reconciliation failure and keep processing
// the rest of the worker batch.
_log.e(
'Failed to reconcile completed native worker item $itemId: $e',
e,
stack,
);
if (_findItemById(itemId) != null) {
updateItemStatus(
itemId,
DownloadStatus.failed,
error: 'Downloaded file could not be added to the Library: $e',
errorType: DownloadErrorType.unknown,
);
_failedInSession++;
}
reconciledIds.add(itemId);
}
}
continue;
}
+12 -2
View File
@@ -537,7 +537,12 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
child: TrackListTile(
track: track,
isInHistory: isInHistory,
onDownload: () => _downloadTrack(context, track),
onDownload: ({bool forceQualityPicker = false}) =>
_downloadTrack(
context,
track,
forceQualityPicker: forceQualityPicker,
),
clickableArtist: true,
leading: SizedBox(
width: 32,
@@ -559,12 +564,17 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
);
}
void _downloadTrack(BuildContext context, Track track) {
void _downloadTrack(
BuildContext context,
Track track, {
bool forceQualityPicker = false,
}) {
downloadSingleTrack(
context,
ref,
track,
recommendedService: _recommendedDownloadService(),
forceQualityPicker: forceQualityPicker,
);
}
+18 -2
View File
@@ -423,7 +423,12 @@ extension _ArtistScreenSections on _ArtistScreenState {
final isQueued = queueItem != null;
return InkWell(
onTap: () => _handlePopularTrackTap(track, isQueued: isQueued),
onTap: () => _handlePopularTrackTap(
track,
isQueued: isQueued,
isInHistory: isInHistory,
isInLocalLibrary: isInLocalLibrary,
),
onLongPress: () => TrackCollectionQuickActions.showTrackOptionsSheet(
context,
ref,
@@ -531,9 +536,20 @@ extension _ArtistScreenSections on _ArtistScreenState {
);
}
void _handlePopularTrackTap(Track track, {required bool isQueued}) async {
void _handlePopularTrackTap(
Track track, {
required bool isQueued,
required bool isInHistory,
required bool isInLocalLibrary,
}) async {
if (isQueued) return;
final settings = ref.read(settingsProvider);
if (settings.allowQualityVariants && (isInHistory || isInLocalLibrary)) {
_downloadTrack(track);
return;
}
final playedLocal = await playLocalIfAvailable(context, ref, track);
if (playedLocal) {
return;
+9 -2
View File
@@ -323,8 +323,13 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen>
child: TrackListTile(
track: track,
isInHistory: isInHistory,
onDownload: () =>
_downloadTrack(context, track, playlistPosition: index + 1),
onDownload: ({bool forceQualityPicker = false}) =>
_downloadTrack(
context,
track,
playlistPosition: index + 1,
forceQualityPicker: forceQualityPicker,
),
leading: track.coverUrl != null
? CachedCoverImage(
imageUrl: track.coverUrl!,
@@ -358,6 +363,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen>
BuildContext context,
Track track, {
int? playlistPosition,
bool forceQualityPicker = false,
}) {
downloadSingleTrack(
context,
@@ -366,6 +372,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen>
recommendedService: _recommendedDownloadService(),
playlistName: _playlistName,
playlistPosition: playlistPosition,
forceQualityPicker: forceQualityPicker,
);
}
+19 -6
View File
@@ -11,14 +11,15 @@ import 'package:spotiflac_android/widgets/in_library_badge.dart';
import 'package:spotiflac_android/widgets/preview_button.dart';
import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart';
/// Track row shared by the album and playlist screens. Tap plays the local
/// copy when one exists and otherwise triggers [onDownload]; long-press opens
/// the track options sheet. Callers supply [leading] (track number, cover
/// art, ...) and choose whether the artist name links to the artist screen.
/// Track row shared by the album and playlist screens. Tap offers another
/// download when quality variants are enabled, otherwise it plays the local
/// copy when one exists and falls back to [onDownload]. Long-press opens the
/// track options sheet. Callers supply [leading] (track number, cover art,
/// ...) and choose whether the artist name links to the artist screen.
class TrackListTile extends ConsumerWidget {
final Track track;
final bool isInHistory;
final VoidCallback onDownload;
final void Function({bool forceQualityPicker}) onDownload;
final Widget leading;
final bool clickableArtist;
@@ -118,7 +119,12 @@ class TrackListTile extends ConsumerWidget {
TrackCollectionQuickActions(track: track),
],
),
onTap: () => _handleTap(context, ref, isQueued: isQueued),
onTap: () => _handleTap(
context,
ref,
isQueued: isQueued,
isInLocalLibrary: isInLocalLibrary,
),
onLongPress: () => TrackCollectionQuickActions.showTrackOptionsSheet(
context,
ref,
@@ -133,9 +139,16 @@ class TrackListTile extends ConsumerWidget {
BuildContext context,
WidgetRef ref, {
required bool isQueued,
required bool isInLocalLibrary,
}) async {
if (isQueued) return;
final settings = ref.read(settingsProvider);
if (settings.allowQualityVariants && (isInHistory || isInLocalLibrary)) {
onDownload(forceQualityPicker: true);
return;
}
final playedLocal = await playLocalIfAvailable(context, ref, track);
if (playedLocal) {
return;