Commit Graph
2411 Commits
Author SHA1 Message Date
vicrodh 051bdbb759 fix(qt): favorite mutations stale the Library feed (#690)
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
2026-08-30 22:17:41 -06:00
vicrodh bbe8b6a0f2 feat(qt): album card quality badges reveal the exact quality on hover
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
2026-08-30 19:55:48 -06:00
vicrodh 0c28f9e64b fix(qt): artist Popular Tracks quality cell mounts the real badge
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
2026-08-30 19:55:48 -06:00
vicrodh c650856c84 feat(qt): picker modal wheel lock, wider card, containment side column
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
2026-08-30 18:58:33 -06:00
vicrodh e9fa3fb57b feat(qt): local rows drag to the sidebar and the queue
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
2026-08-30 18:58:32 -06:00
vicrodh c6ae362b7e fix(qt): converging playlist index; explorer picker; remote rows first-class
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
2026-08-30 18:58:21 -06:00
vicrodh 4984736370 feat(qt): picker modal pins last playlist and shows containment strip
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
2026-08-30 17:23:58 -06:00
vicrodh dade0edb26 feat(qt): membership hydrator, honest picker containment, last playlist
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
2026-08-30 17:23:50 -06:00
vicrodh ee621ae28b feat(library): offline copies match both membership identities
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
2026-08-30 16:41:19 -06:00
vicrodh 44df1e53a4 feat(models,core): playlist revision evidence and track-ids fetch
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
2026-08-30 16:38:13 -06:00
vicrodh e1138756e6 feat(library): playlist membership index substrate
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
2026-08-30 16:37:25 -06:00
vicrodh 9486346130 feat(qt): explorer collapse caret tooltip and collapsed filter strip
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
2026-08-30 16:17:29 -06:00
vicrodh 74a41150e9 feat(qconnect): add transactional LAN delegation
cx
2026-08-30 15:33:23 -06:00
vicrodh f30abd12ac feat(qconnect): add official LAN wire service
cx
2026-08-30 13:50:32 -06:00
vicrodh 52c22006ae feat(qconnect): report real buffer state
cx
2026-08-30 13:34:25 -06:00
vicrodh 9493e96e05 fix(qconnect): align auth and playback wire
cx
2026-08-30 13:08:58 -06:00
vicrodh 78a177a36d fix(qt): stop a DLNA renderer before a new URI; shadow-decode proxied tracks too
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
2026-08-30 12:21:35 -06:00
vicrodh a8796ab91e feat(qt): keep the visualizer alive while casting (shadow decoder)
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
2026-08-30 11:35:57 -06:00
vicrodh d620990dad feat(player,cast): share one copy of in-memory track bytes across consumers
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
2026-08-30 11:35:57 -06:00
vicrodh 657bd9757d fix(qt): cast the clicked track, serve cold tracks progressively, proxy server-streamed sources, stop on exit
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
2026-08-30 10:50:47 -06:00
vicrodh 0451000bf0 test(cast): cover the RangeSource media entry
cc
2026-08-30 10:50:46 -06:00
vicrodh abc125bcfe feat(player): progressive external stream for cast renderers
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
2026-08-30 10:27:31 -06:00
vicrodh 976185912f fix(cast): serve arriving bytes, one thread per request, close the receiver app on disconnect
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
2026-08-30 10:27:31 -06:00
vicrodh c3556fe3ec feat(qbzd): expose ALSA hardware volume controls
Carry hardware-volume capability, selection, diagnostics, and status through the daemon API, CLI, and TUI while preserving fail-closed behavior on unsupported devices.

cx
2026-08-30 09:14:39 -06:00
vicrodh 96342e23bc feat(qt): expose ALSA hardware volume settings
Add capability-aware enablement, exact control selection, diagnostics, live volume synchronization, and translated unsupported-state feedback to the Qt settings surface.

cx
2026-08-30 09:14:29 -06:00
vicrodh 0e8614fe37 feat(audio): add guarded ALSA hardware volume control
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
2026-08-30 09:14:16 -06:00
vicrodh 51b7e012f4 merge: qbzd successor warm-up, streaming gapless hand-off, inhibit backoff
cc
2026-08-29 21:53:08 -06:00
vicrodh a9c886b0d3 fix(media-controls): stop re-asking login1 for an inhibitor every 2 s
`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
2026-08-29 18:35:03 -06:00
vicrodh e90ea8cada fix(qbzd): warm the successor on every track start and stream cold gapless hand-offs
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
2026-08-29 16:11:54 -06:00
vicrodh ba6273a5f1 fix(qml): drop the CastPicker quality height binding loop
cc
2026-08-29 15:31:20 -06:00
vicrodh 51f2803f95 fix(qml): TrackRow emits one playRequested per gesture
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
2026-08-29 15:31:20 -06:00
vicrodh c7be041543 fix(qt): route every audible start through one remote-renderer seam
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
2026-08-29 15:31:20 -06:00
vicrodh f19b870398 fix(dlna): skip SetVolume when the quantized 0-100 level is unchanged
cc
2026-08-29 15:31:20 -06:00
vicrodh 3a52638fce fix(cast): keep an idle Chromecast connected instead of dropping it after 5 s
`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
2026-08-29 15:31:20 -06:00
vicrodh 3dbc1fc9d0 fix(qt): restore playlist header menu
cx
2026-08-29 14:55:30 -06:00
vicrodh 9e1f2c5559 fix(playback): recover stalled streams
cx
2026-08-29 14:55:21 -06:00
vicrodh 8964544891 chore(deps): reqwest 0.13 (rustls-no-provider; query/form are opt-in features now)
cc
2026-08-29 09:00:56 -06:00
vicrodh 3785a7eb40 chore(deps): rfd 0.17 (xdg-portal; the async-std feature is gone)
cc
2026-08-29 08:53:42 -06:00
vicrodh 6d8c951a38 chore(deps): rusqlite 0.40 (u64 casts; a latent two-statement execute fixed)
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
2026-08-29 08:36:21 -06:00
vicrodh d89397415b chore(deps): mdns-sd 0.21 (ScopedIp addresses)
cc
2026-08-29 08:26:25 -06:00
vicrodh 1beb061198 chore(deps): mpris-server 0.10
cc
2026-08-29 08:26:25 -06:00
vicrodh 51e496e47f chore(deps): tokio-tungstenite 0.30
cc
2026-08-29 08:26:25 -06:00
vicrodh 957ce924b3 chore(deps): prost 0.14
cc
2026-08-29 08:24:28 -06:00
vicrodh 0b4c9e28b5 chore(deps): base64 0.23
cc
2026-08-29 08:24:28 -06:00
vicrodh 06453f8768 chore(deps): roxmltree 0.21
cc
2026-08-29 08:24:28 -06:00
vicrodh 0cded1e395 chore(deps): rand 0.10 everywhere (qbz-player, qbz-secrets were on 0.9)
RngCore/fill_bytes -> RngExt/fill, the trait names rand 0.10 exports.

cc
2026-08-29 08:24:28 -06:00
vicrodh c4d3fc7e3b chore(deps): cargo update — 194 semver-compatible bumps
cc
2026-08-29 08:23:36 -06:00
vicrodh a1763df250 merge: release workflows green on every platform (desktop-entry gate, Icon Composer icon, arm64 runner)
cc
2026-08-29 07:54:17 -06:00
vicrodh 37a1a75de1 ci(linux): pass QMAKE to cxx-qt-build; fix(player): macOS match covers StreamType::Direct
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
2026-08-28 21:03:47 -06:00
vicrodh 7b05ef55ae fix(listen-log): a gapless hand-off closes the previous row as natural, not skip
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
2026-08-28 20:50:26 -06:00