The Library feed loaded behind a pure once-per-session latch: a
favorite added anywhere mid-session updated every glyph cache but the
grid membership only changed on restart — or a language switch, the
only other reload_library caller. The latch is now a staleness flag:
every mutation through the fav_cache funnel (album/track/artist/label
hearts, playlist hearts) resets it, and the next Library visit
refetches. Deliberately not a live republish (the toggle_pin doctrine);
in-view bulk actions already reload directly. The reporter's
survives-restart half remains unexplained by code and needs his data.
cc
QualityBadge has always owned a hover tooltip, but every AlbumCard host
passed only the tier, so the tip had nothing to say. The card gains a
qualityDetail input feeding the badge's label, and all fifteen mounts
now pass their model's detail (HomeCard rides its qualityLabel twin).
Un-hydrated Plex/remote rows surface their bare filetype — the known
honest fallback; an empty detail still shows no tooltip at all. The
Scene panel's own card already wired this and is untouched.
cc
The PopularTrackRow fork rendered a lone tier word (HI-RES/CD) in a
plain Text while the document has carried qualityDetail all along; it
now mounts the same bare QualityBadgeFull as the shared TrackRow's
quality cell — tier label over the exact bit-depth/rate line. Audit:
every other popular/track list already goes through rows/TrackRow.qml,
so this fork was the only divergent quality cell.
cc
The picker and its duplicate sub-modal eat the wheel on scrim and card
(the DiscoverConfigModal contract) so the page behind no longer
scrolls. The card widens to 735px, and Already in moves from an in-flow
horizontal strip to a right-hand column that exists only when there are
containments to show — the playlist dropdown overlays the main column
only, so the answer stays readable while the list is open.
cc
The shared drag gains a LOCAL payload arm: a Local Library / Explorer
row enters via dragStartLocal carrying its ROW ID, resolved to
source-aware refs at drop time — a library row id can never be read as
a catalog id, the same invariant the picker's Payload enum enforces.
TrackRow's localDragSource reuses every existing guardrail (horizontal
arming, select-mode/dead-row/playBlocked gates, preventStealing,
cancel teardown) without touching catalogRow's meaning. Drops route
through the picker's own add matrix: refs onto a local playlist or the
Qobuz sidecar, and Qobuz ids onto a local playlist (previously a
documented POC gap); queue drops land at the exact slot through the
same insert path as Qobuz drags. Native-catalog tracks rows keep the
gesture off until their identity can ride it.
cc
The membership hydrator looped forever on one playlist (smoke
2026-08-30): playlist/get and getUserPlaylists do not report comparable
updated_at stamps, so the revision evidence never settled, and the
driver had no forward-progress guard. replace_tracks now copies the
HEADER's revision into the membership evidence, staleness requires
comparable evidence on both sides (count is the fallback when either is
missing), and a playlist successfully written in a round is never
fetched again within it — after the initial index build, only the
playlists the authoritative list reports changed re-sync.
Library Explorer rows never opened the picker: the bulk resolver did
not know the explorer's own row caches and dropped every merged
Plex/remote row. It reads them now, and Jellyfin/Subsonic tracks are
first-class playlist citizens: source-prefixed refs (the Plex pattern),
a playlist_remote_tracks sidecar for Qobuz playlists, source-qualified
rows in local playlists, read resolution through the media cache, and
containment arms in the membership index.
cc
The dropdown pins the session's last successful target above the
results (clock glyph, de-duplicated by Rust, retained under a search
only when it matches). The body gains the collapsible Already in strip:
one card per writable playlist already holding carried tracks, with an
N-of-M badge on partial multi-track containment; clicking a card
selects that playlist so completing a partial add is one click plus the
duplicate-confirm's only-new answer. The strip renders only when the
index can back the claim — complete with matches, or updating under an
explicit caveat — and an empty answer from an incomplete index renders
nothing. Four new strings in all eight locales.
cc
playlist_index_qt walks the library's hydration queue one
playlist/get?extra=track_ids fetch at a time (300ms pace, two attempts
per playlist per round, nothing written on failure), poked by every
recorded authoritative list. The sidebar and playlist-manager loads now
record ownership-aware authoritative generations (skipped while the
session user is unknown, and never from a failed fetch). The picker doc
gains lastUsed — session-local, set only by a confirmed successful add
or create, validated against the live row list — plus alreadyIn rows
with distinct-ref counts and the complete/updating/unavailable
tri-state (offline degrades updating to unavailable). Followed
playlists are no longer offered as add targets. Successful add/remove/
create/delete mutations move the snapshot synchronously; local-mode
refs resolve to typed membership refs at the index boundary.
Verified: cargo test -p qbz-library (158) and -p qbz-qt (615) green.
cc
An offline copy (qobuz_download row) attaches to Qobuz playlists as a
library-row sidecar but to local playlists as its catalog id; the
Library ref now carries the optional catalog id so containment looks
both ways. Also adds mark_inactive for the delete-playlist hook.
cc
Playlist and PlaylistWithTrackIds deserialize the API updated_at stamp
(absent on some list shapes, hence optional), and the core facade
exposes the lightweight playlist/get?extra=track_ids fetch the
membership hydrator drives.
cc
Split the snapshot's single timestamp into honest freshness columns
(header_updated_at vs membership_synced_at, plus membership_count and
remote revision evidence), add authoritative-list generations with a
two-generation retirement grace, persist ownership explicitly, and add
incremental add/remove/create producers so successful mutations move
the snapshot synchronously. New playlist_membership module answers the
inverse question — which writable playlists already hold these tracks —
over the snapshot, both sidecar tables and local playlists, through a
typed PlaylistTrackRef instead of the plex:/id string mangling. All
membership tables gain reverse-direction indexes; migrations are
probe-then-ALTER with a legacy backfill test.
cc
The Library Explorer caret now says what it does on hover (Show/Hide
filter panels, all eight locales). While the panels are collapsed with
filters applied, a single-line strip of removable chips stays visible
next to the caret (top of the results area for the left/right rail
layouts), so hiding the panels never hides what is filtering the
results. The chip is the empty-state delegate extracted into a shared
component; removal reuses clearFilterChip verbatim.
cc
gmediarender ignored a SetAVTransportURI issued while it sat in
PAUSED_PLAYBACK (2026-08-30 smoke: a Plex row did not replace the Qobuz
item until Next), so a loaded renderer is stopped before the new URI.
Server-streamed (Plex/Jellyfin/Subsonic) tracks now also feed the
visualizer: a second, sequential download of the item into a progressive
buffer (LAN traffic, capped at 400 MB) gives the shadow decoder the bytes
the Range proxy never holds; cancelled with the track like the Qobuz one.
cc
The FFT/scope surfaces are fed by the local engine's writer thread, which
is stopped while a renderer plays, so every visualizer animation froze
the moment a cast started. cast_viz decodes the same bytes the renderer
is served (progressive download buffer, cached track or local file)
silently on its own thread and pushes them into the VisualizerTap at
real-time pace, re-anchored to the renderer's reported position on each
poll (re-seek past 0.75 s of drift, paused with the renderer). Proxied
server-streamed tracks have no local bytes and stay dark.
cc
InMemorySource::from_shared decodes an Arc<Vec<u8>> without copying and
MediaServer::register_audio_shared serves the same Arc, so the cast media
server and the cast visualizer's shadow decoder can sit on one copy of a
100 MB track.
cc
Four findings from the 2026-08-30 casting smoke, each with its cause:
- A Local Library click while casting played the OLD queue's current
track on the renderer: that funnel stages its queue after the audible
step, and the gate routed "the current track". route_play_remote_track
hands the renderer the clicked track itself.
- Plex/Jellyfin/Subsonic rows were refused ("serves it over the network").
The media server now proxies the source url with the ticket's request
headers (exact size from the probe), closing the TODO the Slint
service carried.
- A cold Qobuz track waited for the whole download before the renderer
heard anything (5.5 s for 97 MB; every frontend did this). Uncached
tracks are served progressively from the player's download buffer;
cached ones keep the instant byte path. The download is cancelled when
the track changes, on disconnect and on shutdown.
- Closing the app could leave the renderer playing: the poll held the
service lock across renderer round trips and the 2 s bounded shutdown
timed out before sending Stop. The poll handle now lives outside that
lock and is aborted first. Chromecast: a toggle with nothing loaded
casts the current track instead of asking an empty receiver to play,
and IDLE counts as "ended" only after the track was seen PLAYING (an
idle receiver app auto-advanced the whole queue).
cc
Player::open_external_stream starts the CMAF download and returns a
shared buffer (exact final FLAC size known from the segment table) once
the first segments are in, instead of after the whole file. The finished
download is cached like a played stream. Core exposes it as
open_external_stream_resolved.
cc
MediaServer gains a RangeSource entry whose bytes may still be arriving
(known final size, blocking reads) and serves every request on its own
thread so a slow media body no longer stalls the HEAD probe or a seek's
second Range request. Chromecast: play/pause/stop never launch the
Default Media Receiver (an app launched with nothing loaded sat IDLE and
read as "track ended"); disconnect stops the receiver app instead of
leaving it on screen with the last item.
cc
Carry hardware-volume capability, selection, diagnostics, and status through the daemon API, CLI, and TUI while preserving fail-closed behavior on unsupported devices.
cx
Add capability-aware enablement, exact control selection, diagnostics, live volume synchronization, and translated unsupported-state feedback to the Qt settings surface.
cx
Probe exact mixer element identities, retain stable device routes, and fail closed for ambiguous or switch-only controls. Synchronize player volume through ALSA events without changing direct PCM configuration.
cx
`SleepInhibitor::acquire` runs on every Playing state update and retried
the `Inhibit` call each time it failed. On a headless daemon with no
logind session polkit answers `InteractiveAuthorizationRequired` forever,
which produced one WARN every ~2 s (369 lines in 11 min on the Pi smoke,
2026-08-29). Classify the failure: a policy denial is logged once at info
and never retried for this process; a transient error backs off 5 min.
Playback was never affected.
cc
The daemon only prefetched the next track after a gapped advance, so every
edge after a seamless hand-off (and every QConnect/API play) reached the
10 s gapless window cold and had to materialize the whole next file in
time. Whether it made it was file size vs bandwidth (#699 follow-up on a
1 GB Pi: gapless "sometimes, varies by album"; alternating pattern).
Add a `WarmSuccessor` driver action fired once per current track id on
the first tick that sees it, for every entry point, run as a driver-owned
task that never blocks the control loop; drop the awaited prefetch from
`advance_and_play`. For a successor that is still not cached at arm time,
append its initial CMAF buffer as an incremental stream
(`queue_gapless_streaming`, the desktop's hand-off) and keep the
whole-file byte fetch only as fallback.
Tests: 2 new driver tests (warm once per track for any entry, seamless
edge warms the new successor); qbz-app 269 and qbzd 298 pass.
cc
A double-click on a clickPlays row fired playRequested three times
(two releases plus doubleClicked) and restarted the track three times
within 230 ms. Debounce the release and skip the doubleClicked arm on
clickPlays rows.
cc
The cast gate lived only in `play_queue_track`; every other entry point
(single track, album, artist, track lists, playlists, MyQBZ, the Local
Library funnel) only checked the QConnect peer, so clicking a track while
casting started the local backend beside the renderer (DLNA played one
track, the host another). `route_play_remote` (cast, then peer, then
local) is now the single gate, called from all 12 entries and from
`play_audible`; `route_play_to_peer` is private. A tripwire warning in
`play_resolved_offline_aware` names any future entry that skips it.
cc
`get_media_position` returned `NotConnected` whenever no media app had
been launched yet, and the 1 Hz poll folded that into "no answer", so a
connect made while paused hit LOST_POLL_MAX and disconnected after
exactly 5 s (reproduced three times on the 2026-08-29 smoke). Add a
typed `CastError::NoMediaSession`, and while idle prove liveness with
the receiver-level `get_status` request instead of the media session.
Also coalesce cast volume: a slider drag now hands the newest level to
one sender task every 120 ms instead of one Cast/SOAP round trip per
pixel.
cc
rusqlite 0.40 removed ToSql/FromSql for u64 (SQLite integers are i64):
every u64 column now reads/writes through an explicit i64 cast. It also
rejects a multi-statement execute() instead of silently running only the
first statement — which is what catalog.rs's editions/logical_albums
prune had been doing all along; it is an execute_batch now, so the
logical_albums prune finally runs.
cc
aarch64 runner: cxx-qt-build reported QtMissing with a correct
QT_ROOT_DIR — set QMAKE/CMAKE_PREFIX_PATH explicitly (as macOS/Windows
already do) and print the qmake version before the build.
macOS: StreamType::output_sample_rate (macOS-only) matched Rodio only;
Direct is intentionally not cfg-gated, so the macOS build failed with
E0004. A direct sink answers with its opened rate.
cc
Under gapless the engine never reports a not-playing tick between two
tracks, so the poll loop's track_ended edge does not fire and the next
track's edge found the previous row still open and closed it as skip
(smoke: 229 s of a 230 s track recorded as skip). The Qt edge now uses
the tracker's inference (last observed position within 2 s of the
duration = natural), the same rule qbzd already applies.
cc