Skip to content

feat(download): implement download resume support#178

Open
Eutalix wants to merge 22 commits intozarzet:devfrom
Eutalix:feat/download-resume
Open

feat(download): implement download resume support#178
Eutalix wants to merge 22 commits intozarzet:devfrom
Eutalix:feat/download-resume

Conversation

@Eutalix
Copy link

@Eutalix Eutalix commented Feb 24, 2026

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 Range header to the provider, and appends the remaining bytes instead of restarting.

Implementation Details

I modified the DownloadFile function in the Go backend for Qobuz, Tidal, Amazon, and YouTube providers (go_backend/*.go).

The logic flow is now:

  1. Check for existing file: Before starting, the backend checks os.Stat(outputPath).
  2. Range Header: If the file exists, a Range: bytes=N- header is added to the request.
  3. Handle Response:
    • 206 Partial Content: Open the file in APPEND mode and continue writing.
    • 200 OK: Server rejected resume (or file didn't exist); truncate and start from scratch.
    • 416 Range Not Satisfiable: File is likely already complete; mark as success.
  4. Error Handling: On network failure, the partial file is not deleted automatically, allowing the user to retry and resume later.

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.

zarzet and others added 20 commits February 20, 2026 14:18
… 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
…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.
@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43f2a9d3-d807-4567-989e-1c94d7348d7b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Eutalix Eutalix force-pushed the feat/download-resume branch from d049397 to 050dc8f Compare February 24, 2026 22:08
Copy link
Owner

@zarzet zarzet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 just isResuming := false
  • Qobuz: redundant req.Header.Set("User-Agent", ...) before DoRequestWithUserAgent() 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.

@zarzet zarzet force-pushed the dev branch 3 times, most recently from 4decdb5 to 98abaf6 Compare March 3, 2026 19:02
Eutalix added 2 commits March 4, 2026 18:16
Fixes critical progress bar bug by passing startByte to ItemProgressWriter. Restores file cleanup on critical IO errors to prevent corruption. Code cleanup and optimizations.
@Eutalix
Copy link
Author

Eutalix commented Mar 4, 2026

@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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants