The download- and metadata-provider priority pages were 99% identical. Extract
ProviderPriorityListPage taking the provider-list load/save callbacks and
labels; both pages become thin wrappers. (The metadata page's explicit
saveLabel was the same dialogSave the scaffold already defaults to.)
Delete the verbatim-duplicate LocalLibraryLookup class + provider, the unused
localLibrarySummaryProvider, notifier search()/getCount()/existsInLibrary
wrapper, and the now-orphaned LibraryDatabase.search(); the notifier's async
getById/findExisting lookups (the real ones) stay. Drop dead
download-history removeBySpotifyId/getDatabaseCount and the orphaned
history deleteBySpotifyId, and fold the byte-identical addToHistory/
adoptNativeHistoryItem into one _persistHistoryItem keeping distinct log text.
Delete dead extension_provider members (ensureSpotifyWebExtensionReady,
enabledExtensions, getExtension, resolveProviderDisplayName, URLHandler.matchesURL,
Extension.hasBrowseCategories) and collapse resolveEffectiveDownloadService/
MetadataProvider into one predicate-parameterized helper.
TrackState.searchArtists/searchAlbums/searchPlaylists were only ever written
empty (the metadata-search parse loops iterated const [] lists and customSearch
set []), so the home feed's separate artist/album/playlist result sections
never rendered. Drop the fields, the dead parse loops and _parseSearch* helpers,
the SearchArtist/Album/Playlist models, the sort caches/methods, and the
_Search* result widgets. The live tracks->buckets sections (_CollectionItemWidget
via artistItems/albumItems/playlistItems) and the search filter tabs are kept.
local_album and the queue batch action carried byte-identical ~110-line copies
of the FFmpeg re-enrich apply+SAF-writeback+temp-cleanup flow (differing only in
helper names). Extract one applyFfmpegReEnrichResult() so the SAF temp-file
cleanup — the class of bug fixed in fb7d101 — has a single source. The
track-detail copy stays: it drives snackbars and does not preserve metadata.
The sheet watched the whole playbackStateProvider, which re-emits every 500ms
during playback, rebuilding the entire ReorderableListView twice a second just
to recompute an unchanged shuffle toggle. Watch a .select of shuffleMode only.
Remove the never-called checkAvailability chain end to end (Dart bridge +
provider, Android/iOS MethodChannel handlers, Go exports) and stop restoring
its persistent lookup cache on every cold start; the legacy prefs key is
cleared on next cache reset.
Delete dead Go: romaji.go (test-only), the no-op pre-warm chain
(PreWarmTrackCache/PreWarmCache/…), retired Download stubs, and a batch of
exports with no Dart/Kotlin/Swift/Go caller. Kept GetTrackCacheSize/
ClearTrackIDCache (Settings cache screen), SetAllowedDownloadDirs and
Clear/GetItemProgress (live test seams).
Dedup: single updateFlacVorbis + replaceFlacPictures helper behind the six
FLAC tag writers; merge byte-identical extractCommentFrame/extractLyricsFrame
into extractLangTextFrame.
go build/vet + 237 go tests, flutter analyze + tests green.
The DB-backed pagination migration left the old in-widget filter/unification
pipeline behind three unused_element ignores; every helper, cache field, and
isolate entry point below them was reachable only from that dead root.
While the home feed loads, show section-shaped skeletons (album cards +
artist circles) instead of the generic track-row skeleton, and shimmer
each explore cover while its image loads — matching the artist screen.
Two holes let a provider's verification requirement vanish so the fallback
chain failed outright instead of opening the challenge:
- classifyDownloadErrorType did not recognize 'signed session expired'
(the exact error ensureSignedSession returns), so an expired session
failed generically.
- Extensions that catch the needsVerification response internally (e.g.
qobuz-web's availability search) report a plain 'not available', which
the fallback loop skipped past silently.
The runtime now remembers when a signed-session call inside the current
script invocation required verification; the provider wrapper consumes
that after checkAvailability/download and tags the failure as
verification_required, regardless of how the script handled it. The
source-extension path also classifies thrown errors now.
Deezer localizes artist and genre names by IP geolocation when no language
is given, so users behind e.g. Arabic-region IPs got Arabic metadata on an
English device (VPN 'fixed' it). The app's display language is now pushed
to the backend and sent as Accept-Language, with an en-US default.
Fixes#480
Reads REPLAYGAIN_TRACK_GAIN (album gain fallback) from FLAC/MP3/M4A tags
and converts Opus R128_TRACK_GAIN/R128_ALBUM_GAIN (Q7.8, -23 LUFS) to the
same ReplayGain dB representation on the Go side, then attenuates the
internal player's volume per track. Off by default; toggle lives under
Library > Playback. Opus header output gain is left to the decoder, which
already applies it.
Closes#465
Decrypt can emit a raw FLAC stream; the AC-4 repair step fed it to the
MP4 box parser, which failed with a garbage atom type whose control
bytes then broke the unescaped Kotlin error JSON (FormatException in
Dart). Guard the repair call by extension, sniff the box type in Go as
a second layer, and build platform error JSON with JSONObject.quote.
Both call sites used a bare dart:io HttpClient with no timeout, retry, or
cache; downloadCoverToFile provides all three (plus the singleflight cover
cache) and can hang-proof the embed path.
- Resolve over DNS-over-HTTPS (1.1.1.1/8.8.8.8, cached, SSRF-filtered)
when the OS resolver fails, on all transports and the uTLS dial path,
so DNS-level ISP blocking no longer kills the connection outright
- Flush the pooled uTLS h2 conns from CloseIdleConnections so a network
switch no longer leaves the first bypass request hanging on a dead conn
- Abort retry backoff sleeps on context cancellation
- Cap honored Retry-After at 2 minutes
- Restore the response body consumed by the 403/451 marker scan
Fixes the random gobind crash 'bulkBarrierPreWrite: unaligned arguments'
(golang/go#46893; cgo aligns exported-function argument frames since
1.26.0). The earlier 1.26 rollback (cdc58367) was the arm32 SIGSYS from
probing futex_time64 under seccomp on Android <=10 — fixed in 1.26.3 by
gating time64 syscalls on kernel >= 5.1 instead of probing (verified in
the 1.26.5 runtime source, which names Android 8-10 explicitly).
go directive carries the full patch version so gomobile's synthesized
bind module picks the same toolchain.
The sigma-18 BackdropFilter re-filters everything scrolling underneath
every frame. The startup runtime profile now feeds a lowEndDevice
provider; those devices get an opaque bottom bar instead.
Flutter reflows in one frame on rotation, so the new layout popped in;
tablets change the most (bottom bar to rail, two-pane now-playing).
A 300ms fade above the router runs on orientation flips and on width
changes across the 600dp rail breakpoint (fold/unfold, split-screen).
- NavigationRail scrolls inside its SafeArea when the viewport is too
short for four labeled destinations (landscape phone with the mini
player), instead of overflowing by its natural height
- the PageView subtree carries a GlobalKey so swapping the body
between rail and bottom-bar layouts at the 600dp breakpoint moves
the element instead of remounting it - rotation no longer snaps
back to the home tab and kept-alive tab state survives
A single HeroMode above the router disables shared-element flights
app-wide (mini player expand, track metadata cover) for users who
prefer no motion or want to save the overlay rendering cost. On by
default; persisted in AppSettings.
- SettingsGroup centers itself at 720dp via its incoming constraint
(not screen width, so groups inside clamped sheets are never
over-inset; unbounded-width contexts guarded); priority scaffold,
bespoke hero cards, and donate page get the same inset - all 18+
settings pages clamp from shared widgets
- queue list-mode, local/downloaded album track lists, favorite
artists, home recent-access, and extension detail sections adopt
wideListInset like the screens fixed earlier
- track metadata content clamped at 720dp, which also tames the
width-derived spectrogram height
- announcement dialog capped at 480dp wide; setup wizard and
tutorial content at 560dp with a 400dp action button
The list-to-header cover flights read as a gimmick and add per-frame
cost during navigation. Removed the heroTag plumbing end to end
(recents, search rows, discography cards, Album/Artist/Downloaded
headers, motion banners). Kept: the mini player expand hero, the
library-to-track-metadata hero, real loading headers, and the
loading crossfade.
An ISRC identifies a recording, not a release, so identifier-first and
search matches could resolve to a compilation of the same song and
overwrite the file's album, cover, track position, and date with the
compilation's. When the file already knows its album and the candidate's
album doesn't match, those release-scoped fields are now preserved;
recording-level fields (title, artist, ISRC, genre) still enrich.
- shell switches to a NavigationRail at >=600dp (same destinations,
badges included); the bottom bar and its blur stay phone-only while
the mini player remains anchored bottom in both modes
- now-playing gains a two-pane landscape layout (artwork left,
metadata/controls right) built from the same widget pieces
- track lists on album/playlist/library-folder screens and search
result sections center at 720dp via wideListInset instead of
stretching across the full tablet width; explore carousels stay
full-bleed intentionally
- bottom sheets capped at 640dp via bottomSheetTheme (centered on
wide screens, no effect on phones)
- update dialog and SAF migration dialog text capped at 480/400dp
- selection action cards and service chips grow from 2 to up to 4
columns based on available width instead of stretching
The subtitle/meta placeholders were shorter than the real artist and
meta rows, so the cover shifted ~12px during the loading-to-content
crossfade and read as a ghosted double image.
Albums/artists with a motion banner have no square cover, so the Hero
had no destination: the open flight ended nowhere and there was no
return flight. The Hero now rides the full-bleed banner, flying the
still cover image as the shuttle so the video doesn't restart mid-air.
With cached albums the fetch finishes mid-Hero-flight; the crossfade
then mounted a second hero with the same tag, which the flight doesn't
adopt — a static cover appeared next to the flying one. The extension
screens now keep the loading header until the route animation completes.
Opening an extension album/artist showed a bare skeleton, so the cover
had no Hero destination and only flew on the way back. Loading states
now render the actual header (title + Hero cover in its final slot,
skeleton placeholders for the remaining rows) above the shimmering list.
SkeletonCrossfade mutes the outgoing child's heroes to avoid duplicate
tags if the user pops mid-fade.
Playlist tracks were read newest-first (added_at DESC, rowid DESC),
so a batch import - inserted in playlist order with one timestamp -
came back reversed, and the in-memory prepend matched that. Playlists
now use insertion order: snapshot reads ASC, single adds append, and
batch adds append in order. Existing rows were already stored in
playlist order, so old libraries display correctly without migration.
The preview-cover query follows the first visible track.
The full-bleed cover made the Hero flight from square list thumbnails
morph into a fullscreen rect and back. The header now blurs the cover
as a backdrop and shows a centered rounded square (album header layout),
so the flight stays square end to end.
The banner only reacted to app lifecycle, so the HEVC decoder and HLS
stream kept running behind covered routes and inactive tabs. It now
follows TickerMode: the Navigator already disables it for covered
routes, and the shell disables it for non-current tabs (which also
mutes stray animations on kept-alive pages).
Button/back pops no longer slide the sheet down: it dissolves where it
stands while the Hero artwork flies back to the mini player. Drag
dismissals keep sliding from the finger's position since freezing a
half-dragged sheet mid-screen would look broken.
After the content fade, popping left an empty dark scaffold traveling
the full screen. The sheet now fades out over the second half of the
pop; opening and dragging keep it fully opaque.
The app bar and body slid up as a block while the Hero artwork flew its
own path, so the title/controls looked detached from the cover. Content
now fades in over the last 60% of the transition (and fades out first on
pop/drag), leaving only the sheet surface and the flying cover in motion.
Tab navigators get a HeroController (nested navigators have none, so
Heroes never flew inside tabs). Search result rows, recent-access items,
and artist discography cards tag their covers; Album/Artist/Downloaded
screens accept a heroTag and mount the matching Hero on the header
cover. Tags are scoped per list section to stay unique per page.