Prevent premature 'downloading' status before actual byte transfer
starts, and cache async provider values to avoid UI flicker during
queue library reloads.
Progress pipeline:
- StartItemProgress now initializes with 'preparing' status instead
of 'downloading'
- SetItemProgress ignores synthetic pre-download progress updates
while status is still 'preparing' (no byte data yet)
- DownloadService reads backend status field and propagates preparing/
downloading/finalizing to native worker item snapshot
- Dart progress stream maps 'preparing' to DownloadStatus.downloading
with progress 0.0 (indeterminate spinner)
Queue tab:
- Add _queueLibraryCountsCache and _queueLibraryPageDataCache to
retain last successful data during FutureProvider refetches
- Prevents empty-state flash when loadedIndexVersion bumps trigger
provider invalidation
- Caches trimmed to max 24 entries via FIFO eviction
Long track names (especially CJK/emoji) could exceed filesystem limits
when used as SAF document display names, causing write failures.
- Add truncateUtf8Bytes in Go, Kotlin (MainActivity + SafDownloadHandler),
and Dart to truncate strings at valid UTF-8 codepoint boundaries
- Limit SAF filenames to 180 UTF-8 bytes (preserving file extension)
- Limit SAF directory segments to 120 UTF-8 bytes
- Fix Go sanitizeFilename to use UTF-8 aware truncation instead of
byte slice which could split multi-byte characters
- Add Go test for multi-byte truncation correctness
- Sanitize SAF relative directory in Dart native worker and regular
download paths via _sanitizeSafRelativeDir
All search, metadata, and download providers are now exclusively
supplied by extensions. The built-in provider registry that previously
exposed Deezer/Tidal/Qobuz as hardcoded providers is fully removed.
Removed across Go, Dart, Kotlin, and Swift:
- BuiltInProviderSpec class, registry, and all accessor helpers
- SearchProviderAllJSON, GetBuiltInProvidersJSON, ParseProviderURLJSON,
ParseDeezerURLExport Go exports and their platform channel bindings
- Built-in provider items in search dropdown, service picker, and
provider priority UI lists
- provider_ui_utils.dart helper file
Deezer metadata enrichment (ISRC lookup, extended metadata, cover
upgrade) remains fully functional through direct DeezerClient calls
in the download pipeline — these are not part of the provider
registry and are unaffected.
Mark deezer as a retired built-in metadata provider so stale user
priority lists are cleaned up on next launch.
Introduce a service-owned download worker that offloads the full
download-and-finalize pipeline to DownloadService on Android, keeping
downloads alive independently of the Flutter UI process.
Key changes:
- Extract SAF download logic from MainActivity into SafDownloadHandler
- Add NativeDownloadFinalizer for Kotlin-side decryption, format
conversion, metadata embedding, ReplayGain, post-processing, and
history persistence
- Extend DownloadService with native queue management (start, pause,
resume, cancel) using coroutine-based worker with AtomicFile snapshots
- Add Dart-side orchestration: snapshot polling, run-id correlation,
adoption on app restart, and fallback to Dart queue
- Forward embedReplayGain, tidalHighFormat, and postProcessingEnabled
through Go backend DownloadRequest struct
- Add nativeDownloadWorkerEnabled setting with UI toggle
- Make DownloadQueueLookup collections unmodifiable
- Add 16 Go supplement test files covering extension runtime, providers,
health checks, lyrics, metadata, HTTP utils, library scan, and more
- Add Dart model/utils test suite (test/models_and_utils_test.dart)
- Update settings.g.dart with deduplicateDownloads serialization
- Add stopProviderFallback manifest field with backward compat for skipBuiltInFallback
- Expose stop_provider_fallback in extension JSON API
- Unify service and search provider chips into 4-per-row grid layout
- Enable Ask Before Download for extensions with quality options
- Force useExtensionProviders always-on (built-in providers retired)
- Update localization: Built-in -> Legacy, remove obsolete description text
- Clear hardcoded donor names list
- Add Duration field to AudioQuality for FLAC (streaminfo) and M4A (mvhd atom)
- Expose duration via ReadFileMetadata and extension runtime Go-backend API
- Pass duration_ms to extension CheckAvailability for better track matching
- Fix home tab showing empty state before extensions finish initializing by
keeping the search bar visible with a loading indicator until ready
- Refactor hasSearchProvider helper to account for built-in providers
- Refine homeEmptyTitle/Subtitle copy (EN + ID translations)
- Bump version to 4.5.0+127
- Replace hardcoded provider prefix checks with resolveEffectiveMetadataProvider using replacesBuiltInProviders manifest capability
- Add preparing/downloading/finalizing progress status constants and SetItemPreparing/SetItemDownloading APIs
- Expose setDownloadStatus to extension JS runtime for fine-grained progress control
- Skip lyrics search for instrumental tracks detected by title heuristic
- Pass tidal/qobuz IDs to extension checkAvailability for richer matching
- Add shouldAbortCancelledFallback helper for robust cancellation propagation
- Add resolvePreferredTrackIDForExtension for intelligent track ID selection per extension
- Remove ambiguous Auto/Default search provider option, always resolve to concrete provider
- Add tests for shouldAbortCancelledFallback and progress status transitions
Delete the entire Qobuz downloader implementation (qobuz.go, qobuz_test.go)
including all API clients, search, metadata, download, and track matching
code. Empty the builtInProviderRegistry now that all built-in providers are
retired. Remove Qobuz-specific exports (SearchQobuzAll, GetQobuzMetadata,
ParseQobuzURLExport) and the downloadWithBuiltInQobuz adapter. Stub out
PreWarmTrackCache and cache management since no built-in providers remain.
Move qobuz cover upgrade regex to cover.go. Update Dart screens, providers,
and localization strings for the provider-agnostic UI.
Empty the builtInProviderRegistry now that all built-in providers are
retired. Introduce isRetiredBuiltInDownloadProvider and
isRetiredBuiltInMetadataProvider to classify deezer/qobuz/tidal/spotify
as retired, replacing ad-hoc string checks. Add Dart-side metadata
provider priority reconciliation that replaces retired providers with
extensions declaring replacesBuiltInProviders. Remove getQobuzMetadata
from native bridges and platform_bridge.dart. Update crowdin.yml with
additional locale mappings.
Remove Tidal from built-in provider registry (metadata, search, download,
URL parsing) and delete tidal.go. Introduce extension runtime APIs for
lyrics lookup (getLyricsLRC), ISRC existence check (checkISRCExists), and
ISRC index management (addToISRCIndex). Refactor extension download response
construction into normalizeExtensionDownloadResult/overlayExtensionDownloadMetadata
helpers with AlreadyExists support and ISRC indexing. Switch download mirrors
to DoRequestWithUserAgent for ISP blocking detection. Add 50+ new
localization keys and accessibility labels across all supported locales.
Replace hardcoded Tidal/Qobuz switch/case with builtInProviderSpec registry
pattern. Unify searchTidalAll/searchQobuzAll into searchProviderAll,
getDeezerMetadata/getTidalMetadata/getQobuzMetadata into getProviderMetadata,
and parseDeezerUrl/parseQobuzUrl/parseTidalUrl into parseProviderUrl. Remove
extension-specific getAlbum/Playlist/ArtistWithExtension in favor of generic
getProviderMetadata routing. Extract provider UI helpers into
provider_ui_utils.dart. Preserve track_number fallback for zero-value
TrackNumber in album/playlist track lists.
- Add reference-counted cancel entries to prevent premature cleanup when multiple operations share the same itemID
- Propagate cancellation to DownloadTrack, DownloadWithFallback, DownloadWithExtensionsJSON, extension providers, and ISRC search
- Fetch album artist from MusicBrainz when missing during download and re-enrich
- Make ALBUMARTIST tag nullable to avoid writing artistName as album artist
- Add home feed 'Off' option in extension settings
- Skip deezer in download provider priority sanitization
- Add monochrome.tf and samidy.com Tidal API mirrors
- Guarantee resolvedProvider is never null by defaulting to 'tidal'
- Replace stale 'Deezer' default label with 'Tidal' (Deezer moved to extension)
- Show dynamic provider target in auto label for search dropdown
- Bind cancel context to all extension HTTP calls (fetch, httpGet, httpPost,
httpRequest, fileDownload, authExchangeCodeWithPKCE) so in-flight requests
are aborted when user cancels a download
- Make initDownloadCancel idempotent: return existing context if entry already
exists and preserve pre-cancelled state
- Force SAF output filename to match actual file extension when extension
returns a different format than requested (e.g. FLAC requested but M4A produced)
- Map ALAC/AAC quality to .m4a instead of falling through to default .flac