feat(download): implement download resume support#178
feat(download): implement download resume support#178Eutalix wants to merge 22 commits intozarzet:devfrom
Conversation
… media controls Implement end-to-end music streaming: Go backend streaming resolver, PlaybackProvider with queue/shuffle/repeat, full-screen player with cover art and word-by-word synced lyrics (Apple Music style gradient sweep), and media notification with skip controls. Refactor Tidal/Qobuz/Amazon resolve logic into reusable functions for both download and streaming paths. Add Android AudioService integration, iOS background audio, and seek-not-supported error handling for live decrypted streams. Bump version to 4.0.0-alpha.1+100.
…ion (v4.0.0-beta.1) - Add Interaction Mode setting (Downloader/Streaming) across all screens - Tap tracks to play instantly in Streaming Mode with full queue support - Add onLongPress track options sheet on album, artist, home, playlist, search - Add USDT TRC20 wallet to donate page with tap-to-copy - Improve mini player layout and lyrics font sizes - Remove stop button from media controls, fix queue exhaustion state - Add localization for all new strings across 14 locales - Update CHANGELOG.md for v4.0.0-beta.1
…mprovements - Add tappable queue indicator (1/50) that opens queue bottom sheet - Queue sheet shows Played, Now Playing, Up Next sections with tap-to-jump - Add playQueueIndex() public method for jumping to specific queue index - Stop playback and clear queue when switching from streaming to downloader mode - Add beta build warning to CHANGELOG.md with Telegram and GitHub issue links - Various UI refinements across screens and widgets
…d race condition guards - Persist playback queue and position to SharedPreferences for restore on app restart; add dismissPlayer() to fully clear persisted state - Add play request epoch tracking to prevent race conditions when rapidly switching tracks - Resolve local library files for queue items (ISRC, track+artist match) so offline tracks play from disk instead of re-streaming - Support Tidal DASH segmented streams via local FFmpeg proxy - Strict emoji/symbol-only title matching (Tidal + Qobuz) to prevent false positives like mapping ringed planet to 'Higher Power' - Add album-based search queries for emoji-only titles to improve recall - Enrich stream request metadata (duration, title alias, artist, album) from Deezer when fields are missing or non-alphanumeric - Shuffle-aware queue display order in queue bottom sheet - Lyrics generation tracking to invalidate stale lyrics fetches - Resume position support for queue playback after restore
- Try native DASH playback first by writing decoded manifest to temp file and passing file:// URI directly to just_audio, avoiding FFmpeg overhead - Fall back to FFmpeg DASH tunnel automatically if native playback fails - Prefer FLAC format for hi-res Tidal streams (>16-bit or >48kHz) - Replace fixed 700ms delay with proper session state polling and timeout for more reliable FFmpeg tunnel startup detection - Add stopNativeDashManifestPlayback() cleanup in all lifecycle points (stop, dismiss, dispose, source switching)
…e prefetch, and optimize rebuilds Platform layer (Android + iOS): - Add EventChannel-based streaming for download progress and library scan progress, replacing Dart-side timer polling with native push - Poll at 800ms on native side with payload dedup to minimize bridge crossings Go backend: - Cache private IP DNS lookups with TTL to avoid repeated net.LookupIP - Add in-memory cache with RWMutex for extension storage and credentials (lazy-loaded, avoids repeated file I/O on every get/set/has call) - Use streaming io.Copy for extension file copy instead of ReadFile+WriteFile - Prefer youtubeMusic URLs over plain youtube in SongLink resolution - Add SpotubeDL engine v3 for MP3 requests Playback (playback_provider.dart): - Adaptive prefetch system: per-service latency tracking, configurable thresholds, retry cooldown, attempt limits per track index - Prefetch DASH manifests without activating (registerAsActive: false) - AppLifecycleListener to auto-save playback snapshot on background - Shuffle toggle preserves already-played order - Disable seek for YouTube streams - Fix durationMs: track.duration (seconds) properly converted to ms Download queue (download_queue_provider.dart): - Use EventChannel stream for progress updates with 3s bootstrap timeout and automatic fallback to timer polling on stream failure - Idle polling throttle (poll every 3rd tick when no active downloads) - Safer deserialization with num?.toInt() casts for stream payloads Local library (local_library_provider.dart): - EventChannel stream for scan progress with fallback to polling - Throttle scan progress notifications by percent change and heartbeat UI optimizations: - home_tab: Move recent access view to Riverpod provider, removing manual identity-based caching from widget state - search_screen: Selective watches on trackProvider fields - library_tracks_folder_screen: Select only items to reduce rebuilds - queue_tab: Instance-level filter content data cache with identity tracking to avoid recomputation across rebuilds - store_tab: Cache filteredExtensions in local variable - mini_player_bar: Proper scrubbing state (thumb stays during drag) - track_collection_quick_actions: Use rootContext for snackbars/dialogs to survive bottom-sheet dismissal; fix streaming mode action labels Services: - ffmpeg_service: Track prepared DASH manifests separately from active; add activatePreparedNativeDashManifest() and cleanup helpers - platform_bridge: Add downloadProgressStream() and libraryScanProgressStream() EventChannel bindings - downloaded_embedded_cover_resolver: Async preview file validation instead of blocking existsSync() - track_provider: Robust duration parsing (duration_ms as num or String)
…aching to reduce rebuilds home_tab: - Wrap search filter bar, recent access, explore sections, and search results in Consumer widgets so each only rebuilds when its own data changes instead of the entire home tab - Extract _SearchResultBuckets with identity-based caching to avoid recomputing track/album/artist/playlist splits on every build - Extract _resolveSearchFilters() helper to reduce inline logic - Move hasActualResults into a single selective ref.watch queue_tab: - Cache filtered grouped albums/local albums with identity tracking (_resolveFilteredGroupedAlbums) to avoid refiltering on every build store_tab: - Replace single ref.watch(storeProvider) with selective watches on individual fields (extensions, selectedCategory, searchQuery, etc.) - Wrap search TextField in ValueListenableBuilder to avoid setState on every keystroke - Change _buildEmptyState to take named parameters instead of full state mini_player_bar: - Extract _MiniPlayerProgressBar as separate ConsumerWidget so position/ duration updates only rebuild the progress bar, not the entire player - Use selective watch on MiniPlayerBar for currentItem, isPlaying, isBuffering, isLoading, hasNext, repeatMode, error, errorType - Extract _localizedPlaybackErrorFromRaw to work with raw strings instead of requiring full PlaybackState
…reens and landscape
…sts discovery - Implement logistic regression model that learns from user behavior (listen ratio, skip patterns) to predict track preferences - Add related artist discovery via Spotify and Deezer APIs with method channel handlers on Android and iOS - Score candidates using 8 features: same_artist, same_album, duration_similarity, source_match, release_year_similarity, artist_affinity, source_affinity, novelty - Diversity-aware selection (max 2 per artist, weighted random sampling) - Auto-refill queue when <=2 tracks remaining, up to 40 auto-adds per session - Persist learned weights, artist affinity, and source affinity to SharedPreferences - Add smartQueueEnabled setting with toggle in options settings page - Only active in streaming mode
… Smart Queue strings - Replace the Spotify API key step in the setup wizard with a mode selection step (Downloader vs Streaming) using consistent _StepLayout - Add _ModeCard widget with radio-style selection, icons, and feature bullet points for each mode - Default to downloader mode; selection saved via setInteractionMode - Add 13 new localization keys (setupMode*, settingsSmartQueue*) to all 17 ARB files with translations for de, es, es_ES, fr, hi, id, ja, ko, nl, pt, pt_PT, ru, tr, zh, zh_CN, zh_TW - Replace hardcoded Smart Queue strings in options settings with l10n - Fix deprecated Radio widget API by using Icons instead
Add always-enabled OnBackPressedCallback in MainActivity to ensure back presses reach Flutter. Nested tab navigators incorrectly set frameworkHandlesBack(false), disabling Flutter's own callback and causing the system default (finish activity) to run. Also fix Flutter-side back handling: restructure _handleBackPress() to clear search and recent access in a single step, fix unfocus-before-clear ordering, preserve isShowingRecentAccess across search/customSearch/ setTracksFromCollection state transitions, and add debug logging. Additional changes: - Smart Queue: add session profiling, tempo continuity, year cohesion, hour affinity, skip streak tracking, dual-source blending, and artist baseline counts for better auto-curation - Playback: route notification controls through dedicated handlers to support resume-from-idle and add error logging - Extensions: add ensureSpotifyWebExtensionReady() for auto-installing spotify-web extension during first setup - HomeTab: trigger explore fetch on mount via post-frame callback
…t queue optimizations - Replace synchronous storage.json writes with 400ms debounced flush to reduce I/O during rapid storageSet/storageRemove calls - Flush and close storage flusher on extension unload; call cleanupExtensions on Activity destroy and AppLifecycleListener detach - Smart Queue: query secondary source only when primary returns insufficient results instead of always running both in parallel - Infer seekSupported early for queue items (disable for YouTube and live-decrypted streams) - Add relaxed fallback artist-repeat limit when strict selection yields no candidates - Add extension_runtime_storage_test.go
- Remove eager lyrics prefetch on every track change to avoid unnecessary network calls - Add ensureLyricsLoaded() method with lifecycle-aware guard (skip when app is paused/hidden) - Trigger lyrics fetch from full-screen player only when visible and app is resumed - Deduplicate prefetch calls using track key to prevent redundant requests
- Add autoSkipUnavailableTracks setting (default: on) to skip to next queue item when stream resolution fails instead of stopping playback - Extract _handleQueueItemPlaybackFailure to centralize resolve error handling and auto-skip logic - Wrap prefetched stream re-resolve in try/catch to handle secondary failures gracefully - Add settings toggle UI with localized strings for EN, ID, and all supported languages
…ion to 4.0.0-beta.1 - Add download icon button in top bar next to lyrics toggle for quick track downloading - Use Expanded layout to keep queue chip centered regardless of button count - Button respects askQualityBeforeDownload setting and hidden for local files - Bump version from 4.0.0-alpha.1 to 4.0.0-beta.1 in pubspec.yaml and app_info.dart
- Use playback_types alias for RepeatMode to avoid ambiguity with Flutter's RepeatMode from repeating_animation_builder.dart
- Add embedMetadata setting as master toggle to skip all metadata/cover/lyrics embedding in both Go backend and Flutter - Guard cover fetch, lyrics fetch, genre/label embed, and external LRC save behind embedMetadata flag - Disable embed lyrics and max quality cover toggles in UI when metadata embedding is off - Fix play button not restarting track after playback reaches completed state - Use ProviderScope.containerOf in track options sheet for reliable provider access from bottom sheet context
Added Range header support and file append logic to Go backend providers (Tidal, Qobuz, Amazon, YouTube) to allow resuming failed downloads.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
d049397 to
050dc8f
Compare
zarzet
left a comment
There was a problem hiding this comment.
Audit Review
The concept is solid and addresses a real pain point (issue #120). However, there are several issues that need to be fixed before merging.
1. Critical Bug: Progress Bar Broken During Resume
ItemProgressWriter.current starts at 0 (see progress.go:203), but SetItemBytesTotal is set to startByte + ContentLength. This means during resume, the progress percentage will be wrong.
Example: Resuming a 100MB file from byte 90MB:
BytesTotal= 90MB + 10MB = 100MB (correct)BytesReceived= 0 → 10MB (only new bytes, wrong)- Progress shows: 10% instead of 100%
Fix: NewItemProgressWriter needs to accept a startByte offset, or initialize current with startByte so that SetItemBytesReceivedWithSpeed reports the correct cumulative bytes.
2. Removing cleanupOutputOnError for flushErr/closeErr is risky
If the buffer fails to flush or the file fails to close, the partial file is now kept but in a corrupted state. On the next retry, this corrupt file will be "resumed" from, producing a permanently broken output file. These errors should still trigger cleanup.
3. HTTP 416 handling needs validation
Returning nil (success) on 416 assumes the existing file is complete and valid. But the file could be corrupted from a previous failed write, or from a different download URL/quality. At minimum, if you know the expected file size (from a HEAD request or cached metadata), you should validate the file size before accepting it as complete.
4. No integrity check on partial file
Before appending, there's no check that the existing partial file was from the same download URL or quality setting. If the user changes quality and retries, the file will be half one quality and half another — producing a corrupt audio file.
Minor
var isResuming bool = false→ idiomatic Go is justisResuming := false- Qobuz: redundant
req.Header.Set("User-Agent", ...)beforeDoRequestWithUserAgent()which overrides it anyway
Summary: Good direction, but the progress bar bug is critical and the cleanup removal needs reconsideration. Please fix these issues and I'll re-review.
4decdb5 to
98abaf6
Compare
Fixes critical progress bar bug by passing startByte to ItemProgressWriter. Restores file cleanup on critical IO errors to prevent corruption. Code cleanup and optimizations.
|
@zarzet Hey! I’ve finished reviewing the PR and applied all the changes you requested. When you get a chance, could you take another look and let me know if everything’s good now? |
Related Issue
Closes #120
Description
This PR implements download resumption for failed or interrupted downloads. This addresses the "Ragebait" scenario mentioned in issue #120 where a large FLAC file fails at 99% and restarts from zero.
Now, if a download fails due to network issues, the partial file is preserved. When the user clicks "Retry", the backend detects the existing file, sends a
Rangeheader to the provider, and appends the remaining bytes instead of restarting.Implementation Details
I modified the
DownloadFilefunction in the Go backend for Qobuz, Tidal, Amazon, and YouTube providers (go_backend/*.go).The logic flow is now:
os.Stat(outputPath).Range: bytes=N-header is added to the request.APPENDmode and continue writing.Note on Frontend (Dart):
No changes were required in Dart (
download_queue_provider.dart). The current implementation already preserves the file path upon failure, so the new backend logic picks it up automatically on retry.