- sharedTransport ResponseHeaderTimeout 45s -> 120s: downloads ride
this transport and some providers prepare files server-side before
the first byte; the stall watchdog still bounds dead transfers
- uTLS pool: a request that fails on a pooled (possibly silently
dead) connection now re-dials once when the body is repeatable,
matching the old dial-per-request reliability
- cover singleflight completes via defer with a default error so a
panicking fetch can never strand waiters in (nil, nil) or leave a
permanent in-flight entry
Stability:
- transient timeouts no longer classified as ISP blocking; they fall
through to retry backoff (hard blocks - DNS/RST/cert - still abort)
- connectivity change now closes idle Go connections in every network
mode (debounced), so pooled sockets from the old interface are not
reused after a wifi/cellular handoff
- download body reads get a 60s stall watchdog that cancels and
surfaces a retryable network error instead of hanging; distinct
from user cancellation
- ResponseHeaderTimeout 45s on all transports; IdleConnTimeout 90->60s;
dial timeout 30->10s; retry backoff gains full jitter; Retry-After
honored on 5xx and Deezer 429
Data usage / speed:
- cover downloads deduplicated with singleflight plus a 24MB/15min
in-memory cache keyed by final URL (album batches fetched the same
1800px cover once per track before)
- song.link availability cached (30min positive / 5min negative)
in front of the 9-req/min rate limiter
- uTLS Cloudflare path now pools one HTTP/2 connection per host with
a shared TLS session cache instead of a full handshake per request
Deezer artist track-count N+1 kept: counts feed the discography
download UI and are already amortized by the artist TTL cache.
- drop unused widgets (AnimatedStateSwitcher, GridSkeleton,
adjacentHorizontalPageRoute, buildRemovalAnimation, SwingIcon,
BottomSheetOptionTile) and empty _buildInfoCard placeholders
- drop test-only Go wrapper methods (GetDownloadURL, MatchTrack,
CheckAvailability 7-arg, CustomSearchForItemID) and their parsers
- drop unused httputil getters/validators and hand-rolled base64
decoder in favor of encoding/base64
- remove unused riverpod_generator dev dependency
Resolve API (api.zarz.moe):
- Refactor songlink.go: Spotify URLs use resolve API, non-Spotify uses SongLink API
- Add SongLink fallback when resolve API fails for Spotify (two-layer resilience)
- Remove dead code: page parser, XOR-obfuscated keys, legacy helpers
Multi-artist tag fix (#288):
- Add RewriteSplitArtistTags() in Go to rewrite ARTIST/ALBUMARTIST as split Vorbis comments
- Wire method channel handler in Android (MainActivity.kt) and iOS (AppDelegate.swift)
- Add PlatformBridge.rewriteSplitArtistTags() in Dart
- Call native FLAC rewriter after FFmpeg embed when split_vorbis mode is active
- Extract deezerTrackArtistDisplay() helper to use Contributors in album/playlist tracks
Code cleanup:
- Remove unused imports, dead code, and redundant comments across Go and Dart
- Fix build: remove stale getQobuzDebugKey() reference in deezer_download.go
- Add displayVersion getter using kDebugMode: debug shows 'Internal', release shows actual version
- Defer Spotify URL resolution in Deezer downloader until fallback is actually needed
- Unify download timeouts to 24h constant (connection-level timeouts still protect hung connections)
- Fix context shadowing in track metadata options menu and delete dialog
- Use addPostFrameCallback + mounted guards for safer sheet/dialog navigation
- Add multi-select support to library_tracks_folder_screen (wishlist, loved,
playlist) with long-press to enter selection mode, animated bottom bar with
batch remove/download/add-to-playlist actions, and PopScope exit handling
- Create batch showAddTracksToPlaylistSheet in playlist_picker_sheet with
playlist thumbnail widget and cover image support
- Add playlist grid selection tint overlay in queue_tab
- Optimize collection lookups with pre-built _allPlaylistTrackKeys index and
isTrackInAnyPlaylist/hasPlaylistTracks accessors
- Eagerly initialize localLibraryProvider and libraryCollectionsProvider
- Enable SQLite WAL mode and PRAGMA synchronous=NORMAL across all databases
- Go backend: duplicate SAF output FDs before provider attempts to prevent
fdsan abort on fallback retries; close detached FDs after download completes
- Go backend: rewrite compatibilityTransport to try HTTPS first and only
fallback to HTTP on transport-level failures, preventing redirect loops
- Go backend: enforce HTTPS-only for extension sandbox HTTP clients
Changes:
- Fix FFmpeg crash issues during M4A to MP3/Opus conversion
- Add format picker (MP3/Opus) when selecting Tidal Lossy 320kbps
- Fix Deezer album blank screen when opened from home
- LRC file generation now follows lyrics mode setting
- Version bump to 3.3.5 (build 70)
- Fix m4aPath calculation in downloadFromManifest for HIGH quality
- When outputPath is already .m4a, use it directly instead of appending .m4a
- Reset httputil.go to fix build errors from merge conflict
- Add genre, label, copyright fields to ExtTrackMetadata and DownloadResponse
- Add utils.randomUserAgent() for extensions to get random User-Agent strings
- Fix VM race condition panic by adding mutex locks to all provider methods
- Fix Tidal release date fallback when req.ReleaseDate is empty
- Display genre, label, copyright in track metadata screen
- Store extended metadata in download history for persistence
- Add trackGenre, trackLabel, trackCopyright localization strings
- Add extension manager, manifest, runtime, providers, settings
- Add extension provider and UI pages (extensions, detail, priority)
- Add download service picker widget
- Add metadata provider priority page
- Add source field to Track model for extension tracking
- Add skipBuiltInFallback manifest option to skip built-in providers
- Update download queue to use source extension first
- Add extension upgrade support without data loss