- DownloadWithExtensionFallback now immediately surfaces verification_required
when any provider needs verification (availability + download stages),
instead of letting later providers mask it
- classifyDownloadErrorType treats 'session is not authenticated' as
verification_required (Go + Dart side)
- parseExtensionAlbumValue.withTrackFallbacks() derives album artist,
release date, and audio traits from tracks when album-level missing
- albumAudioTraitsFromTracks detects dolby_atmos/hi_res_lossless/lossless
from per-track audio_quality/audio_modes fields
- parseBitDepthSampleRate parses '24bit/96kHz' style quality labels
Add a MotionHeaderBanner (video_player) that plays a looping muted HLS header video with a static image fallback for artist, album, and playlist screens. The Go backend now exposes header_video, header_image, and audio_traits from extensions. Album/playlist headers show the release year and Dolby Atmos / Lossless badges inline, full date and song count in a footer, a centered square cover when no video is present, and a full-bleed video when one is.
Cache health results after each check, add a force flag to bypass the cache when the download picker opens or a service is selected, guard stale concurrent results via a per-extension request serial, and treat DNS lookup failures as indeterminate/transient.
- Queue/Library: switch from growing-limit refetch to offset-based append pagination per filter/search/sort, with page cache reassembly and protected-page eviction to avoid top-of-list gaps
- Queue/Library: watch only the active filter page instead of all/singles/albums simultaneously; filter mode follows PageView swipe and tab tap
- library_database: combine three union-based count queries into a single round-trip
- library_scan: parallel scan of non-CUE audio files with a bounded 2-4 worker pool, preserving order, progress, and cancellation; CUE handling stays sequential
- extension_manager: cache compiled goja.Program per extension and RunProgram per isolated download instead of re-reading and re-parsing index.js
Add a setting that relaxes the SSRF guard so extensions and built-in network code can reach private/local/loopback targets, for users routing traffic through a local proxy or custom DNS. Disabled by default. Wired end-to-end: Go backend (SetAllowPrivateNetwork toggles isPrivateIP guard), Android/iOS platform bridge, Dart settings model/provider, and a toggle in Download settings.
Flutter: bump connectivity_plus, device_info_plus, share_plus, receive_sharing_intent, path_provider, flutter_local_notifications to latest stable. Revert file_picker from 12.0.0-beta to 11.0.2 stable and migrate pickFile (singular) to pickFiles. Add win32 ^6.0.1 dependency_overrides to resolve the win32 5 vs 6 conflict between file_picker stable and device_info_plus (Windows-desktop-only transitive dep, not used on mobile targets). Go: update goja, golang.org/x/mobile, golang.org/x/tools, regexp2/v2 to latest.
Add a playlist position token usable in filename templates, plumbed from the playlist screen through the download queue to the Go backend. Auto-prefix playlist downloads with the position when the template lacks the token. Includes Go filename test.
Classify verification-required and rate-limit errors from extensions, propagate Retry-After seconds through the download fallback, report session-grant success/failure, and add a completeGrant action fallback in the runtime.
Decrypt AC-4 via the FFmpeg mov muxer with a -f mov fallback, then repair the output to a standards-compliant ISO MP4: inject the dac4 config box from the encrypted source, normalize the QuickTime container/sample entry, and write iTunes metadata (incl. cover and lyrics) natively. Codec-keyed and generic, so it applies to any extension that returns AC-4 streams. Wired through PlatformBridge/MainActivity for both SAF and local decrypt paths.
Re-encode/downscale cover art that exceeds the FLAC 24-bit picture block limit (go-flac silently truncated it into a corrupt file). Locate ilst in both ISO and QuickTime-style meta atoms, and skip freeform tags gracefully when no iTunes container exists.
FFmpeg's MP4 muxer drops ISRC and label, so write them as iTunes freeform atoms natively after every M4A embed pass. The metadata screen now reads all edited fields from the file on load (file is the source of truth), fixing edited values reverting to stale cached values after reopening.
When the chosen download service matched the track's source extension it was skipped in both the source preflight and the fallback loop, so downloads silently fell back to another provider. It is now attempted in the loop, and an explicitly selected provider bypasses the fallback allow-list.
- iOS: begin/end UIBackgroundTask while a download queue is active so in-flight downloads survive backgrounding for the limited window iOS allows
- extensions: serialize install/upgrade/remove in the Go manager (mutationMu) and in the Dart store provider to stop concurrent goja VM teardown/reload from hard-crashing the app
- main: add runZonedGuarded + FlutterError/PlatformDispatcher onError so uncaught Dart errors are logged, not fatal
- batch convert sheet: precompute localized title/label before showModalBottomSheet to avoid Localizations lookup via a deactivated context
WAV/AIFF: library scan, quality probe, native tag read/write via embedded ID3 chunk (RIFF id3 / AIFF ID3), cover art, ReadFileMetadata, ExtractLyrics, and FLAC<->WAV/AIFF conversion (PCM, bit-depth preserved via ffprobe). Treat WAV/AIFF as lossless across all convert sheets (no bitrate picker, Lossless labels) via isLosslessConversionTarget. Native MIME maps for SAF. Redesign the track metadata three-dot menu to a settings-style grouped card with a single divider above Share.
LyricsPlus (KPOE): word-by-word synced lyrics with multi-server failover, converted to enhanced LRC. ReplayGain: standalone EBU R128 (re)scan writing REPLAYGAIN_TRACK_* tags via native writers or FFmpeg, with batch action in queue/album screens and SAF support.
Bump riverpod, go_router, sqflite, permission_handler, ffmpeg_kit, flutter_local_notifications, json_annotation and riverpod_generator/lint to stable; refresh go.mod/go.sum via go get -u.
- Rename downloadProviderMatchesBuiltIn -> downloadProviderReplacesLegacyProvider
- Rename Tidal DASH ffmpeg helpers and lossy format pickers to generic names
- Add utils.decryptCTRSegments crypto API + raw/bytes file read path in extension runtime
- Update l10n strings/descriptions to drop hardcoded service names
- Bump version to 4.5.7+134
- Dart: _metadataMatchIsConfident now handles album-only case (title empty)
by adding albumMatches fallback branch
- Go: selectBestReEnrichTrack treats placeholder values (Unknown Title,
Unknown Artist) as empty via isPlaceholderReEnrichValue, so album-based
fallback filtering works correctly
- Add test for placeholder album fallback in selectBestReEnrichTrack
- Re-enrich: reject candidates that don't match title/artist/album unless exact ISRC match
- Respect settings.embedLyrics instead of hardcoding true in re-enrich flows
- Skip lyrics resolution in NativeDownloadFinalizer when not needed
- Apple Music lyrics: use direct catalog API with token scraping instead of Paxsenix search
- Support ELRC/ELRCMultiPerson/Plain formats in Apple Music lyrics response
- Add confidence check in metadata auto-fill to prevent applying wrong metadata
- Add tests for stricter re-enrich matching logic
Go backend:
- Add AudioCodec field to DownloadResult and DownloadResponse
- Extension download results can now include audio_codec/audioCodec
- ffmpegGetInfo and probeAudioQuality now return codec field
- Add trackItemBytes option to file.download() for custom progress handling
Flutter:
- Check audio_codec before container conversion
- Skip FLAC conversion if source codec is lossy (AAC, MP3, Opus, etc.)
- Prevents fake upscale from lossy to lossless containers
- Extract _formatDownloadProgressLabel() for cleaner code
- Show received/total size when bytesTotal is available
- Estimate total size from progress when only bytesReceived is known
- Add text overflow handling with ellipsis
New lyrics providers using Paxsenix API:
- Spotify: Synced lyrics from Spotify
- Deezer: Synced lyrics from Deezer
- YouTube: Lyrics from YouTube
- Kugou: Lyrics from Kugou (Chinese service)
- Genius: Plain text lyrics from Genius
Implementation:
- Add lyrics client implementations for all providers
- Smart search result scoring based on track name, artist, and duration
- Support for both synced (LRC) and unsynced lyrics formats
- Fallback search with simplified track names and primary artist
UI updates:
- Add provider entries to lyrics priority settings page
- Add display names for new providers in settings
The HIGH-quality lossy format picker can now produce an AAC/M4A 320 kbps output alongside MP3 and Opus. FFmpegService.convertM4aToLossy/convertAudioFormat, the Dart queue pipeline, the Kotlin finalizer, and the library database format helper all route .m4a through a unified aac codec path and tag the resulting file with the M4A metadata writer. The Lossy Format setting gains a new option, and the track metadata convert dialog lists AAC next to the other targets.
Apple Music lyrics gain a 'eLRC word sync' switch (default off). When disabled the pax-to-LRC formatter strips inline word timestamps, producing line-synced LRC that is safer for players that choke on eLRC; enabling it restores the previous word-by-word behaviour. The change propagates through SetLyricsFetchOptions and invalidates the global lyrics cache on toggle.
Broad l10n migration: roughly 400 previously hardcoded English strings across queue, settings, track metadata, repo, audio analysis, setup and extension screens now live in the ARB catalog, with matching plural/placeholder forms. No behaviour change beyond localisation. Existing and new unit tests (lyrics eLRC toggle and Dart settings round-trip) pass.
Bump the history schema on both the Kotlin finalizer and the Dart database to v9, adding bitrate (kbps) and format (codec label) columns, and let the download flow fill them from backend/probe metadata so lossy downloads keep a 'AAC 256kbps' label instead of falling back to the stored placeholder. Library filtering and the track metadata screen now read format/bitrate directly from those columns, which also fixes mis-tagged quality badges after re-downloading a track at a different format.
Additional fixes bundled in: EditFileMetadata now routes ReplayGain writes through the M4A path whenever the file starts with ftyp (fixing .flac files that actually hold MP4 containers); GetM4AQuality falls back to the first trak/mdia/mdhd duration when mvhd is zero so EAC3 streams no longer report 0s; and both Kotlin and Dart reject bitrate values below 16 kbps to prevent probe noise from surfacing as '0 kbps' labels. New unit tests cover the EAC3 mdhd fallback and the mis-named M4A replaygain path.
GetM4AQuality now recognizes fLaC, alac, ec-3, ac-3, and ac-4 sample entries and parses the MP4 FLACSpecificBox so library entries carry the real codec rather than the container extension. The AudioQuality struct exposes Codec and Bitrate fields (with an estimator for compressed streams), and ReadFileMetadata publishes format + audio_codec so Flutter and Kotlin can make format decisions based on the actual stream.
Downstream: library_scan labels M4A-family items as flac/alac/eac3/ac3/ac4/m4a, zeroes the bitrate for lossless formats, and the filter UI + quality badges use the codec-derived format instead of only the file extension. Scans and SAF importers also accept .mp4 and .aac file extensions. New unit tests cover codec name mapping and MP4 FLACSpecificBox decoding.
GetM4AQuality previously defaulted to 16-bit whenever the audio sample entry was not ALAC, which silently labeled lossy AAC downloads as CD quality in the library and in extension APIs. Only fill BitDepth when the atom is ALAC (including the ALACSpecificConfig refinement), and leave it as zero for AAC/mp4a, matching how the MP3 and Opus probes already report lossy sources. Tests cover both the ALAC and AAC branches.
Staged SAF outputs and library-scan partials now share a single naming pattern: '<name>.partial' regardless of the audio extension. The previous '<name>.partial.<ext>' form caused SAF / media-scanner to surface half-written files as valid audio.
- SafDownloadHandler: force 'application/octet-stream' MIME for staged docs and collapse buildStagedSafFileName to '<name>.partial'. Keep the legacy form behind buildLegacyStagedSafFileName and sweep both via deleteStaleStagedFiles so upgrades clean old residue.
- library_scan: add isLibraryStagingFile that skips both the new and legacy partial patterns during collectLibraryAudioFiles so residual staging files never show up in the library.
- library_scan_supplement_test: seed both legacy and new partial files and assert they are ignored by the scanner.
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