New msgids: Show/Hide lyrics translation (toggle tooltips), Translation
language + Auto (account language) (flyout row), and the fail-soft toast
"Translation is not available right now. Please try again later.".
Register-matched per catalog (de/nl informal, fr/pt formal); en follows
its msgstr = msgid convention. msgfmt --check clean on all eight.
km
The commit push now carries translation-available (doc translation_langs
non-empty) and per-line translation-text, aligned 1:1 with the rendered
lines (the core filters translation lines like the originals; a missing
entry degrades to empty). Toggle ON resolves the target (explicit setting,
else Auto: account language_code -> UI locale) and refetches the current
track with LyricsRequest.language set — show-translation flips only when
the response actually embeds a translation; any gap toasts the Android-
parity error string and reverts, never putting the lyrics view in an
error state. Toggle OFF is pure UI (no network). While ON, track-change
requests ride the resolved target so the translation follows across
tracks; prefetch stays original-only. A doc that already carries the
target's translation flips locally without a refetch. The miniplayer
mirrors show-translation so the mini lyrics surface follows the sidebar.
km
Read-only accessor over the session lock — the lyrics translation "Auto"
resolution reads it through the shared client handle. None pre-login and
for pre-v10 persisted sessions.
km
New QbzSelect row (Auto (account language) + en es fr de it pt nl ja ru,
the owner-approved list) after the Size row; language names are native
literals, the same convention as the Appearance dropdown. The options
array lives on LyricsState (reseeded from Rust on language change) and
the choice persists per-user in lyrics_prefs.json as translation_language
("auto" or an ISO 639-1 code), sanitized on load (unknown -> auto) and
round-tripped through apply_to_ui/persist_from_ui like the other prefs.
Flyout popup height grows by the new row (and the comment now counts the
lite-fill row it had missed).
km
Circular button anchored bottom-right of the lyrics body, overlaying it —
mobile parity (owner decision 2026-07-22: floating, NOT in the header,
NOT in the flyout). Mounted only while LyricsState.translation-available,
so a doc without translations renders the sidebar exactly as before.
Off = dim, on = accent; tooltips via the shared TooltipState channel.
The glyph is Lucide's languages icon, matching the bundled icon set.
km
LyricsLineItem gains translation-text (empty when the doc has none) and
LyricsState the translation props (translation-available, show-translation,
toggle-translation callback) per spec section C. LyricsLinesView renders
the translation under its original line while show-translation is on —
smaller (~72%) and dimmer, static text that follows the line; the karaoke
fill stays keyed on the original timings/text. Secondary surfaces share
the model, so the field flows through untouched.
km
SharedClientProviders implements the new language-aware LyricsProviders
boundary; both track-change and prefetch requests pass None (the default
original-only flow per spec section B.1) — the refetch-with-language is
triggered by the UI layer in a follow-up task. The committed LyricsDoc
now carries the translation into CURRENT_DOC automatically, so the sync
engine and the future UI toggle read the same document. qbz-core's
headless get_lyrics wrapper keeps its signature and fetches
original-only.
km
LyricsRequest carries an optional translation target that threads
through the LyricsProviders::qobuz boundary to the client; the external
fallback chain is untouched. LyricsDoc gains translation
(Option<Box<TranslatedLyrics>> with a LyricsKind dispatch mirroring the
Android v10 mapper: wsync->WordSynced, lsync->LineSynced, plain->Plain)
plus a client-derived has_translation (requested lang in
translation_langs, false when none requested). QobuzWsync stores the
translation verbatim in the existing cache JSON (serde default, no DB
migration) with its lang recorded: a synced cache hit only serves a
language request when the stored translation matches it, otherwise the
chain refetches; original-only requests keep today's behavior.
km
parse_login_response now reads user.country_code and user.language_code
(snake_case wire names, verified against Qobuz's own embedded login
fixture) into two new serde-defaulted Option fields on UserSession, so
pre-v10 persisted sessions still load with both as None (translation
stays Auto-off). qbzd's test construction updated for the new fields.
km
get_lyrics_url_raw/get_lyrics_url/get_lyrics take an optional ISO 639-1
language, appended to the signed param list only when Some (the existing
sign_request sort covers it). None keeps the original-only default flow.
When a translation was requested but the document does not embed one,
the envelope's translation_requested.lyrics_url is fetched as a fallback
translation source (spec section B.3); every gap degrades to
original-only. lyrics_probe gains --lang=<iso639-1> for parity.
km
QobuzLyricsDocument gains the optional translation member (same content
union as original, new in API 10.0), the content DTOs become
Serialize+PartialEq so the lyrics cache can persist a translation
verbatim, and a lang() accessor exposes the content language. New
fixtures cover embedded translations of each type (wsync/lsync/plain),
the lyricsUrl envelope with translation_requested, empty
translation_langs (feature off), and 9.x documents (no translation).
km
Integrates the 30 commits that landed after the version bump: Most Played
Albums, album-identity mode, multi-artist header, cross-tag scan (#447,
#507), local album play-next/queue (#636), Plex rating-key resolution,
ALSA unstick, gapless-late resume, DSD/hi-res DLNA (#646), media-server
token redaction, label web parity, macOS chrome/NPB work, qbzd/Connect
hardening, apt trigger (#648) and the flatpak twin talk-name drop. Also
includes the owner's latest TAG_DETAILS touch-ups.
km
Play-count album history store (album_play_history, WAL per ADR-002) hooked
into record_recent, Most Played Albums carousel on Home + For You (default
off), View-all page with search + per-card play count, i18n in all 8
locales.
Conflicts resolved against pre-release: kept the Slint-era Pinned section
alongside the new MostPlayedAlbums everywhere (DiscoverySectionId, prefs,
render kinds, nav, ContentView, HomeActions); AlbumCardItem carries both
is_pinned and plays (populated via card_to_item); .po files union-merged,
msgfmt clean; discover_prefs tests updated (home 15/8 ON, forYou 14/13 ON)
— 10/10 green. Release build green.
km
Session tokens in /audio/<token>/<id> must not appear in log bundles. Port
of the remaining delta of 9aab6233 (pr/605) onto current pre-release: add
redact_media_uri() to media_server.rs and redact the URL in the three
handle_request logs (request, 404 parse, 403 token mismatch). The device.rs
side and the CSPRNG tokens already landed via PR #586.
km
raw-window-handle 0.6 exposes only the content view; dereference the NSView
and get the NSWindow via -[NSView window] instead of reading a non-existent
ns_window handle.
km
The 2px .slint literal proved invisible (owner ask 2026-07-21, iteration
2026-07-22). The extra clearance is now set from Rust at startup
(ShellState.npb-small-extra, default 6px, macOS-only, QBZ_NPB_SMALL_EXTRA
env override) so tuning costs a ~2-minute qbz-bin rebuild instead of a
~2 h qbz-ui one on the 8 GB Mac. Linux stays pixel-identical (extra = 0).
km
The AlbumCard context menu on local albums (LocalLibrary grids, folder or
Plex source) routed play-next/queue to LocalLibraryActions.album-action,
whose handler only implemented play / toggle-select / favorite — the two
queue actions fell into a catch-all that logged "queue slice pending" and
did nothing.
Resolve the album's tracks with the same source-aware resolver play uses
(local_library::fetch_album_tracks_blocking — local folders AND Plex) and
enqueue them via playback::enqueue_local_tracks, mirroring the already-
working albums bulk-action arm. Track-level play-next/queue was never
affected. Port of d0f67149.
km
Port of 0c3b38f8 (bugfix/internal/local-library-tag-fallback) onto current
pre-release. The scanner read metadata from a SINGLE lofty tag
(primary_tag().or(first_tag())) with no per-key fallback to the file's other
tags. Old, repeatedly-retagged collections routinely carry the album /
Album Artist / date only in a secondary tag (ID3v1/APE/Vorbis) while the
primary tag lacks them:
- #447: the missed album tag let the folder-derived backfill land in the
album column and masquerade as metadata (folder-name albums, folder-level
grouping, MIN(year) across mixed folders).
- #507: the missed Album Artist left album_artist NULL, degenerating every
COALESCE(album_artist, artist) to the per-track artist (Various Artists).
extract() now falls back per key across tagged_file.tags() (primary first,
deterministic conflict policy) for title/artist/album/album_artist/genre/
catalog/track/disc/date. Folder backfills stay as-is for genuinely untagged
files. Tests adapted to the AlbumGroupMode signature; 75/75 pass. A full
library rescan picks the fix up (no migration).
km
The MyQBZ collections path stamps the per-item ALBUM boundary key
(plex:<hash>, for album-shuffle boundary detection) in source_item_id_hint,
but the Plex play path used that hint verbatim as the track rating key:
/library/metadata/plex:<hash> -> 404, the Plex track never started, and the
previous track kept playing while the UI showed the new one (continuous
playback Qobuz -> Plex broken, audio/UI desync).
Ignore any plex:-prefixed hint and fall back to the numeric queue id (=
rating key for the common numeric-key case). The LocalLibrary path (hint =
raw rating key, no prefix) is unchanged.
km
Docker/cross build of the slint-free daemon for the Pi streamer
(~90 s incremental, dist/qbzd-aarch64-linux); native path included for
aarch64 hosts.
km
The API serve loop is single-threaded; an inline block_on of the
load leg (next/previous/jump/play/cold-start) starved every route for
the whole fetch — clients read 'daemon not reachable' while the action
DID execute. The resolve+play leg now runs on the tokio runtime and
the routes ACK immediately; cheap gates (auth, 404, empty queue) stay
synchronous with their documented errors, and load failures latch into
last_errors.stream (visible via status). next/previous answer
{"queued":true,"direction":...} (CLI renders '-> queued (next)');
play/toggle cold-start report {"state":"loading"}; jump/play keep
their response shape. Live-verified: next returns in ~20 ms and status
answers during a load.
km
When a track ends before its gapless prefetch completes, 'track
finished (engine empty)' sets thread_state.is_playing=false, and the
late PlayNext still plays (the engine's append re-opens the writer
gate) but nothing restores the player-level flag: the position monitor
freezes, no next gapless arm, no QConnect reports, the queue cursor
stays on the finished track, and playback dies silently at the next
end. The PlayNext handler now captures engine.empty() before appending
and restores is_playing when the append landed on an empty engine —
a user-paused engine still holds its source (not empty), so manual
pause is never resumed by this.
km
Three transition killers on drivers that require an explicit drop
(snd-rpi-hifiberry/PCM5122; the desktop's USB DAC tolerated all three,
which is why only the Pi died):
1. stop() called prepare() without drop(): prepare from
RUNNING/DRAINING returns EBUSY there — every stop failed and left
the pcm in limbo. Now drop+prepare (drop EBADFD tolerated).
2. A write racing a still-draining pcm (natural end + late gapless
append) gets EBADFD, and snd_pcm_recover(77) is unsupported on this
stack, so the write died and the writer thread exited. New
recover_write_error(): normal recover first, then drop+prepare for
EBADFD (11 write sites via one helper).
3. snd_pcm_drain can block forever when the device stops clocking —
the writer wedged and no 'engine empty' ever fired. drain() is now
bounded: poll pcm.state() with a 10s deadline; the tail finishing
shows as XRUN (drop+prepare), and a stuck device is cancelled.
km
CPAL enumerates descriptions, never hw:CARD=... ids, so a playing
direct-hw stream read as 'not present'. device_present is now
device_open || enumeration-match.
km
The daemon was queue receive-only: a CLI/TUI/MPRIS/restored queue never
reached the cloud, so controllers rendered a different queue than the
one playing. New qconnect/publish.rs ports the desktop's
sync_local_queue_if_changed with its full gate set (active-renderer,
offline-only, echo-suppress vs last-applied, per-session pushed-ids
latch, all-or-nothing admission), triggered by a debounced
CoreEvent::QueueUpdated subscriber — which also covers queue edits
while paused/stopped, unlike a transition-only hook. Live-verified: a
CLI queue move produced exactly one QueueLoadTracks push (233 tracks)
and the cloud echo was suppressed.
km
Akamai answers small raw-url objects with ~106 headers, over hyper's
hard-coded 100-header h1 cap, so every reqwest fetch of those tracks
failed identically (streaming probe AND full download) and the track
never played — with the cause hidden by reqwest's Display. Now:
describe_reqwest_error() walks the source chain for logs; on the
header-flood signature the engine skips the doomed reqwest re-download
and falls back to play_track_resolved (CMAF, unaffected) plus an
explicit sync_current_to_id so the queue cursor follows the audio.
Any other failure keeps the previous behavior. Live-verified on the Pi
(DSOTM track 1 plays; gapless continues).
km
With a custom name set, the controller bootstrap announced it but the
deferred renderer join and resolve_local_identity used the DEFAULT name —
one device_uuid, two friendly names. When the server's ADD_RENDERER omits
the uuid (observed on the wire), the name-fingerprint self-match failed,
local_renderer_id stayed None and the is_local_renderer_active gate
dropped every renderer report (mute renderer, frozen controller seekbar).
default_qconnect_device_info() now resolves the persisted name on both
frontends, so all announce paths agree by construction. Wire-verified:
ADD_RENDERER carries the custom name and reports flow.
km
The token encryption key mixed in the XDG portal secret (session-bus
dependent): a login run in a graphical session wrote a token an
init-started/headless qbzd could never decrypt, reported as a bare
"not logged in". PortalKey policy: desktop root keeps Session, daemon
root uses Never; one-shot migration on read (decrypt with Session,
rewrite portal-free); oauth_token_file_present_at() +
latch_undecryptable_token() so status distinguishes "no token" from
"token I cannot decrypt". Live-verified 3x (OpenRC, headless Pi,
migration path).
km
Spec 2026-07-19-local-album-grouping-mode (B2). A compilation album (the
Saint Seiya case: 10 distinct track artists) now shows the single artist
line plus a '+N more artists' chevron that expands into the full
track-artist list, each entry clickable into the LocalLibrary Artists tab
(name-based open-artist). Distinct raw track artists in first-appearance
order, computed in apply_album_version into LocalAlbumState.all-artists.
Expansion is view-local and resets on album change; single-artist albums
never see the chevron. Track rows already showed their own artist
(verified, no change needed).
2 msgids ('+{} more artists', 'Show less') x 8 locales; slintcheck green.
km
Spec 2026-07-19-local-album-grouping-mode (A2-A5 + i18n). The Albums tab
gains a third header dropdown ('Albums by folder' / 'Albums by metadata')
and Settings > Local Library a matching 'Album grouping' row — both write
the same pref: LocalLibraryState.albums-id-mode, persisted in
locallibrary_ui.json (default 'folder': compilations and box sets stay
whole; the metadata split stays opt-in for #411's use case).
The group key changes with the mode, so switching reloads the Albums set
(reload_albums) and invalidates the Artists tab (its album cache groups
the same way — a folder-mode compilation cross-lists under every artist
in all_artists, the owner's 'appears in all artists' ask). Threaded
through spawn_albums_load, seed_counts, the Plex quality-hydration
aggregate lookup, and the artists feed; myqbz_builder pins Metadata
(artist-scoped candidates, not the Albums view).
5 msgids x 8 locales (msgfmt clean); slintcheck green.
km
Spec 2026-07-19-local-album-grouping-mode (A1 + C + D).
AlbumGroupMode { Metadata, Folder }: the Albums view can now group by the
album|artist metadata pair (today's #411 behavior) OR by the album-root
folder — a compilation with 10 track artists renders as ONE card, and
multi-disc boxes merge even when discs are tagged separately. Read-time
switch: no schema change, no rescan. Threaded through
get_albums_metadata_page / count / get_albums_metadata_grouped; the
cover-art lookup matches both key shapes. Title/artist derivation is
mode-agnostic (album tag preferred, album_group_title fallback, per-row
COALESCE(album_artist, artist) + COUNT(DISTINCT track_artist) > 1 ->
'Various Artists').
Untagged-root clamp (C): infer_artist_album no longer returns the library
ROOT's name as the artist when the album dir hangs directly off a root
(a tagless DSD set surfaced with artist 'Music'). extract_with_roots
threads the scan targets; plain extract keeps the legacy inference for
ephemeral/single-file paths.
Tests (66/66): folder mode compilation = 1 album + VA + all_artists,
album_artist respected, orphan bucket preserved, metadata split unchanged;
root-clamp cases incl. disc subfolders, 'Artist - Album' split, real
artist folder named like the root, and the no-roots legacy path.
km
LabelView parity with the revamped Qobuz web label page (semantics
verified live against play.qobuz.com via Playwright):
- Header: Follow + Shuffle (new play_label_top_shuffled, shuffle-then-
delegate to play_tracks_ctx) + overflow menu (Play next / Add to queue /
Share -> play.qobuz.com/label/<id>; no Song.link equivalent for labels).
No header Play by owner decision (the Popular Tracks play does the same).
- The Popular Tracks section '...' button was dead (fired an unhandled
'top-tracks-menu' action): now a popup with Play next / Add to queue
over all cached popular tracks. Section buttons wrapped so the 44px
primary and 32px secondaries share one vertical centre.
- Catalog/library toggle: the ArtistPage SegmentedTabBar verbatim, floated
right of the header cluster (visible only with library items; mirrors
the web's A/B-rolled 'Del catalogo / En la biblioteca'). Library tab
renders added tracks + albums on the label, backed by the new
library_by_label session index (AlbumCard.label_id / TrackCard.label_id,
seeded from the SAME two favorites fetches as library_by_artist).
- More Labels carousel now renders the Library LabelCard (new label-cards
mode in ArtistCarousel) instead of the legacy generic card.
- Artists carousel images: /label/page top_artists often carry no parseable
image, leaving placeholders forever. Restored the Tauri fallback wave:
custom artwork first, then v2/artist/get, fed through the stable
LabelArtist artwork targets.
i18n: zero new msgids (all reused across the 8 locales). slintcheck green.
km
With the custom chrome (titlebar_transparent + fullsize_content_view)
AppKit parks the native traffic lights at the stock ~28pt titlebar height,
visibly above the 42px header controls. macos_chrome.rs measures the close
button's centre in window coordinates and shifts the shared button
container by the delta to the 21pt header centre (custom chrome only,
one-shot after show; known gap: AppKit re-lays the titlebar on fullscreen
roundtrips). Deps: raw-window-handle direct + slint raw-window-handle-06 +
objc2-app-kit NSWindow feature, all macOS-target only.
NPB-Small on macOS gains 2px of bar height (ShellState.npb-small-height,
44px, growing upward from the bottom-anchored bar) so CJK/descender-heavy
meta lines stop clipping at the window's bottom edge. PlayerBarSmall pins
the song card to its old 39px top-anchored geometry: the art keeps its
exact top-relative position and the 2px land as bottom clearance. Linux
geometry is pixel-identical to before. slintcheck stub green.
km
The x86_64 workflow fires the repository-dispatch minutes after its own
publish, while the native arm64 build is still running — so qbz-apt's
update-repo downloaded the Release assets when only the amd64 .deb existed.
The arm64 .deb landed hours later with nothing re-indexing, leaving the
apt arm64 tree stuck at 1.2.15 (verified: QBZ_*_arm64.deb IS on the 2.0.x
releases). update-repo is idempotent (re-downloads every *.deb, rebuilds
both indices), so the second dispatch completes whichever arch the first
one missed, in either finish order.
km
DSD containers (.dsf/.dff) fell through to application/octet-stream in the
media-server MIME map, which strict renderers reject (AVTransport 714), and
the serve path read the WHOLE requested range into RAM before emitting a
single header byte — a 32/384 PCM or DSD track (0.7-1.4 GB) stalled the
response long enough for renderer fetch timeouts and blocked the
single-threaded server for every other request (the reported 'had to
restart QBZ' wedge).
- content_type_for_path + the cast_service UI mirror gain audio/x-dsf /
audio/x-dff (de-facto DLNA MIMEs; HQPlayer plays native DSF/DFF).
- open_range streams file entries straight from disk (seek + bounded Take);
headers go out immediately and RAM stays bounded. In-memory (Qobuz)
entries keep the bounded slice copy. 0-byte entries keep the old 500.
- DSD-aware cast quality label (was the nonsense '1-bit / 2822.4 kHz').
- Best-effort renderer stop() when DLNA load/play fails so it doesn't sit
half-loaded on a faulted URI.
cargo check + 7/7 qbz-cast tests (new streaming + MIME coverage).
km
The What's New / release-notes modal renders the GitHub release body through a
custom block model that stripped inline markup and had no link support, so a
markdown link showed as literal `[text](url)`. Now:
- inline links `[text](url)` collapse to just their text (clean attributions),
- a whole-line link becomes a KIND_LINK block rendered as a clickable accent
link opened in the browser (a new WhatsNewActions.open-url reusing open::that,
mirroring about.rs).
Adds a `url` field to WhatsNewBlock plus a small markdown-link parser. The 2.0.2
notes use it for the daemon-wiki link. Built + smoked at release (window ready,
ALSA 192kHz bit-perfect, playback OK).
cc
Workspace version 2.0.1 -> 2.0.2 (Cargo.toml + lock, workspace crates only —
no dependency re-resolution), a new metainfo <release> entry, and the release
docs: TAG_DETAILS.md (human-facing, About-modal/website format, 2.0.2 section
prepended above 2.0.1/2.0.0) + CHANGELOG.md (full detail since v2.0.1 — the
headless daemon, kiosk mode, dynamic background, cast/quality #638, audio,
rendering #617, library, themes and security work). Codename inherited on the
patch line; About-modal literal untouched.
cc
The `playback.mpris` field added to DaemonPrefs broke the qbz-app roundtrip test
(a `DaemonPrefs { .. }` literal missing the new field) — a compile error the
`test-crates` CI caught but `cargo test -p qbzd` didn't (it doesn't compile
qbz-app's test module). Add the field with a non-default value and assert it
survives the save/load roundtrip. Full non-UI workspace suite green (67 ok/0
failed) via scripts/cargo-test.sh.
cc
Integrates feature/qbzd-console (21 commits): the full headless-daemon CLI
parity surface (search/play/browse/discover/radio/reco/favorites/playlists/
queue/shuffle/repeat/lyrics/art/resolve), Last.fm/ListenBrainz scrobbling
(setup-TUI + CLI + scrobble-on-play + offline queue), MPRIS media controls
(TUI-toggleable, default on), the /api/events SSE stream + `qbzd watch`, GET
/api/artwork/current, and an init service-file generator (systemd/openrc/runit).
qbzd is a pure consumer of the audio crates: verified it still checks and passes
all 287 tests against pre-release's ALSA/DacCapabilities/models/player changes,
so the merge is textually and semantically clean (no conflict resolution needed).
cc
The bubble had a hardcoded 24px height, so the #638 Source/Output/cause tooltip clipped to its middle line. Height now follows the content: multi-line fits, a one-line tooltip stays compact.
cc
The no-art (number) variant drew a hardcoded #ffffff glyph with no backing, so it vanished on light themes (white-on-white). It now sits on a themed circular backing (polarity-baked alpha fill/border + shadow) and tints with Theme.text-primary; the row hover uses the themed alpha ramp too. Zebra left as-is to avoid a dark-theme shift. ADR-008 compliant (icon-only circle, not a pill).
cc
The delivered-first badge, amber downgrade arrow and Source/Output/cause tooltip only lived in AudioStamp (Small/Large/kiosk). The default New mode - and Classic - mount SongCard, whose badge was bound to the catalog max only, so a CD-capped stream showed '24-bit / 96 kHz' with no arrow. SongCard now consumes the same NowPlayingState delivered fields; no new strings (reuses AudioStamp's msgids).
cc
The per-renderer cap dropdown now uses the plain Settings product names (Hi-Res / CD Quality / MP3, looked up by key so they can't drift) instead of the misleading verbose 'CD - 16-bit / 44.1 kHz' form - a CD-tier track can be 48 kHz; the tier is a format_id, not a fixed rate. Removed the '...no 48 kHz option' caption. quality_label_from_track detail is now bit-depth-first to match every other badge.
cc
Learned per-tier ETA from the last successful build, a live elapsed/ETA/percent ticker while codegen is silent, and start/finish banners. Safe under zsh/oh-my-zsh (runs under bash, newline-terminated, TTY-gated colors). NO_TICKER=1 silences it.
cc
The cause-3 line "Limited by the quality cap for this device" only ever
fires while casting, yet it renders in the main window's badge tooltip
where its sibling cause-2 line uses "device" for the local DAC — same
word, two different devices. Interpolate the connected renderer's name
(CastState.device-name), the form the spec's 2.5 copy sketch specifies.
All 8 catalogs updated in place; the picker's "capped for this device"
suffix is unchanged (the device name sits directly above it).
cc
The hydration task's store order (values, then id) was documented as the
guarantee that a reader keyed on the id never sees another track's
params, but all three stores were Relaxed — program order alone does not
survive a weakly-ordered CPU (macOS ARM is a shipped target). Release
the id store and Acquire its load in hydrated_catalog_quality so the
claimed invariant actually holds; the value atomics stay Relaxed.
cc
delivered_tier_str's requested-mp3 shortcut is correct on the local
engine path (an MP3 stream decodes to 16-bit PCM, so the requested tier
is the only tell), but on the cast path a successful STREAMINFO parse
proves the served bytes are FLAC: an over-cap cache/offline serve under
an mp3 cap rendered "MP3" beside a measured "24-bit / 96 kHz" detail.
Derive the tier token from the probed bits whenever the probe read the
bytes; the shared helper still covers the probe-less fallback.
cc
after_track_change runs kick_prefetch even when play_audible routed the
track to a renderer, and the cast auto-advance funnels through the same
local advance. The prefetch fetched upcoming tracks at
local_playback_quality() — the DAC-capped tier — into the quality-blind
L1/L2 cache, and the cache-first cast resolve then served those bytes to
the renderer verbatim: the local device cap leaked into a signal path it
is not part of (precedence rule, owner decision), and the badge
misdiagnosed the resulting downgrade as catalog availability.
Resolve the prefetch tier once per kick_prefetch: the cast-effective
tier (preference clamped by the renderer's manual cap, via the new
casting_prefetch_quality accessor) while a renderer is connected, the
device-capped local resolve otherwise. The single resolve also keeps the
network-throttle bandwidth estimate agreeing with what the prefetch
actually requests. Skipping the prefetch during cast was rejected: the
cache-first cast resolve deliberately consumes it (fast advance, P1).
cc