qbzd can now BROWSE the catalog from the CLI: open an album, an artist page /
top-tracks / releases grid, find similar artists or albums, and pull For-You
suggestions — reads that feed the composition pipeline (`--ids | queue add -`).
- api/browse.rs: GET /api/album (get_album + optional get_album_suggest);
GET /api/artist?view=page|top|albums (get_artist_page / get_artist_tracks /
get_releases_grid); GET /api/similar?artist=|album= (get_similar_artists /
get_album_suggest); GET /api/suggest?seed= (get_dynamic_suggest — seeds from
the current queue when omitted). Typed serde shapes verbatim; auth-gated;
blacklist fail-open.
- cli/browse.rs: `album`, `artist`, `similar`, `suggest`; a generic items/tracks
walk drives the human list and --ids; `--seed -` reads ids from stdin.
- api/mod.rs: P1_ROUTES 2->6 (+album/artist/similar/suggest); routes + test.
main.rs: four subcommands.
Verified: cargo test -p qbzd --bin qbzd -> 262 passed, 0 failed.
cc
qbzd can now START playback of a track / album / playlist / artist / Qobuz
URL from the CLI — the receiver becomes a source. Server-side materialization
and the shipped cold-start ritual; the protected audio path is untouched.
- api/play.rs: resolve selector (a URL via link_resolver wins over id fields),
fetch tracks (get_album / get_playlist / get_artist_tracks / get_tracks_batch),
map via the now-shared queue::track_to_queue_track, stamp album/artist/
playlist provenance, then set_queue -> play_track_resolved -> save_session_now
(never a bare cursor move). Auth-gated; 404 on unknown id / empty resolution;
503 on start failure.
- cli/play.rs: `qbzd play [album:ID|track:ID|artist:ID|playlist:ID|URL|N]`;
bare `play` delegates to transport::play (one resume implementation). Local
exit 2 on a malformed selector.
- api/queue.rs: track_to_queue_track -> pub(crate); api/playback.rs:
resolve_quality -> pub(crate) (both now shared with play).
- api/mod.rs: P1_ROUTES += POST /api/play; route + test. main.rs: Play gains
an optional content arg (bare = resume, unchanged).
Verified: cargo test -p qbzd --bin qbzd -> 256 passed, 0 failed.
cc
First slice of Proposal A (CONSOLE): qbzd can originate discovery — search
Qobuz by name from the CLI, not only receive via QConnect or take track
ids by hand.
- api/search.rs: GET /api/search?q=&type=&limit=&offset=. Typed searches
return SearchResultsPage<T> verbatim under a per-category key; type=all
fans out to the four typed searches. Auth-gated (needs_auth); blacklist
fail-open by design (documented GUI-parity delta).
- api/mod.rs: new P1_ROUTES table + p1_route_table test — the shipped-caller
HARD RULE (§3.1.4) applied to the content-verb door; route registered.
- cli/search.rs: `qbzd search <q> [--type all|albums|tracks|artists|
playlists] [--limit N] [--offset N] [--ids] [--json]`. --ids emits ids
one-per-line (pipes into `queue add -`); frozen exit-code table.
Verified: `cargo check -p qbzd` clean (no warnings); `cargo test -p qbzd
--bin qbzd` → 248 passed, 0 failed (10 new + 238 existing). New files are
standard-rustfmt; existing files carry only the minimal logical edits.
cc
Inert scaffold for #617 partial rendering in the vendored femtovg-wgpu
renderer. Mirrors the Skia renderer: add an ItemTransform to the render
State (logical space), accumulate it in translate/rotate/scale
(pre_translate/pre_rotate/pre_scale), implement current_transform() (the
core ItemRenderer trait default is todo!()), and add
impl ItemRendererFeatures { SUPPORTS_TRANSFORMATIONS = true }.
Nothing consumes current_transform() yet — the core PartialRenderer proxy
is wired in a later stage — so this is behavior-neutral. Not yet compiled;
validates at the stage-2 build.
cc
Issue #637: a Qobuz outage + forced re-login left a user's account
transiently 403'd, and the no-backoff prefetch retry storm escalated it
into a per-IP edge/WAF block (the "error decoding response body" lines are
that non-JSON WAF body hitting a bare .json()). The bundle bump b019->b034
is exonerated: extraction yields byte-identical app_id/secret/private_key.
- ForbiddenBreaker: after repeated 403s the hot streaming/favorites paths
short-circuit without touching the network, with an exponential cooldown
and a half-open probe; success resets it.
- get_stream_url: typed 401/403 + body preview; abort the quality fallback
loop on 403/401/breaker-open instead of firing all five qualities.
- CMAF session/start + file/url: feed the breaker, map 403.
- get_favorites + qws/createToken: check status before .json() so a 403
surfaces as a 403, not a misleading "error decoding response body".
- Prefetch: per-track failure cooldown (negative cache) so a failed track
is not re-hammered every queue tick.
- Audible path: a 403/back-off no longer counts as an "unavailable" skip;
stop cleanly with a toast.
- QBZ_FORCE_403 test hook to smoke the back-off path on a healthy account.
- Header action-button tooltips for AlbumView/ArtistView/PlaylistView,
translated across all 8 locales.
cc
PlaylistCard now reflects ownership: owned playlists show favorite only;
foreign Qobuz playlists show follow/unfollow on the overlay plus follow +
copy in the context menu. Added is-owned/is-following/is-copied to
SearchPlaylistItem, seeded from the favorites owned/followed split and
owner.id == current_user_id elsewhere. Wired the actions id-scoped across
both routing families via shared helpers (copy/follow/favorite), so a card
toggles ITS playlist instead of the open detail.
Library "All" toolbar: genre filter (own "library-all" context over the
mixed feed; albums + tracks carry a genre, non-genre kinds hide when a genre
is selected) and PlaylistView-style sort menu (re-select the active field to
flip asc/desc). Tooltips on the four source toggles.
Live favorite/follow sweep across every card surface that was missed:
album favorite now reaches the Pinned carousel, the Recently Played "view
all" page and the artist "In library" grid; artist follow reaches Home Top
Artists, label-page artists and Pinned; new set_playlist_row_following flips
the follow chip on every playlist card (incl. Pinned) after subscribe.
cc
The artist grid/overlay play button emitted media-action("artist", id,
"play") but no Rust arm handled it, so it was a no-op. Add play_artist:
play the artist's Popular (top) tracks, and when the artist has none, fall
back to playing their studio discography — the "album" + EP/single buckets
in the page's section order, deduped by album id, excluding compilations,
live, and "other". Fetches the artist page once and decides from it; a
single unavailable album is skipped without a toast or aborting the rest.
cc
On Wayland every tray hide->show destroys the winit window and re-runs
the femtovg-wgpu set_surface, which with WGPUConfiguration::Automatic
created a brand-new VkInstance + VkDevice on each restore. The NVIDIA
driver segfaults on that churn: a null call inside libnvidia-glcore,
reached via libvulkan from set_surface before the first render
(reproduced under gdb, close-to-tray -> restore with playback starting
mid-restore; it crashed even while paused, ruling out the shader
drain path).
Create the wgpu instance/adapter/device/queue ONCE at startup and hand
it to Slint as WGPUConfiguration::Manual (Linux+Wayland only), so a
restore only re-creates the surface/swapchain on the SAME device. The
Automatic configuration stays as the fallback on other platforms and
on creation failure.
km
The OSK opened on kiosk search/inputs but never closed: focus stayed on
the search field. Clear focus in the field's `accepted` handler and add a
full-bleed background TouchArea that clears focus on any tap outside the
input, so the on-screen keyboard dismisses.
cc
The std ScrollView does not intercept a touch-drag that starts over the
dense SettingRow controls, so Settings did not finger-scroll on the Pi
kiosk. Replace it with a raw Flickable (intercepts the drag after a few
px) plus a ListScrollbar overlay for the desktop bar; scroll-position
restore now keys on the Flickable's own height.
cc
- Tray restore no longer crashes (RefCell panic on close / segfault on restore):
clear ImmersiveState.shader-texture in tray::hide_window BEFORE the surface is
destroyed (UI thread, no rendering-notifier RefCell borrow), instead of a
deferred clear in the RenderingTeardown notifier that ran too late when the
drain was parked (paused). The 30 fps drain repopulates it after restore.
- All list view: right-click opens the context menu AT THE CURSOR (window
pointer minus the row's absolute position); the whole row is right-click
sensitive (per-cell handlers), while the ⋯ button keeps anchoring the menu to
itself. Hide the source column + grid source badges when show-local is off
(all-Qobuz feed → no per-row Qobuz mark).
- Remove the now-unused FavArtistCard / FavLabelCard components (the Favorites
Artists/Labels grids use ArtistGridCard / LabelCard).
cc
- Immersive exit now returns to the shell (kiosk or desktop) the user came
from, and only drops fullscreen when it was NOT fullscreen before opening —
so the kiosk appliance (which boots fullscreen outside immersive) is no longer
dumped back to the windowed desktop shell on exit. Captured on the open edge
in the fullscreen guard.
- Add a WindowEvent::Touch arm: a one-shot log confirming whether the compositor
delivers native touch (RaspiOS/labwc mouse-emulation withholds wl_touch, which
is why swipe/drag never works — a compositor-side fix, not an app bug), plus
touch-point mirroring to ShellState so hover-revealed chrome reacts under
native touch.
cc
- Rework the "All" list view to the aligned column layout of the mixtape detail
list: artwork + hover play, title + clickable-artist subtitle, a TYPE column,
a dedicated SOURCE column (SourceGlyph), the row QualityBadgeFull in a Quality
column (was the uneven grid badge), favorite/follow indicator, and the per-kind
context menu — no longer bare and out of tune.
- Fix the NVIDIA "wallpaper join" background seam: replace the float hash2 in
ambient.wgsl + plasma.wgsl with a bit-exact integer (u32) lattice hash so the
same cell corner hashes identically from adjacent cells on NVIDIA.
- Fix the close-to-tray "RefCell already mutably borrowed" panic introduced by
the teardown texture-clear: defer the set via upgrade_in_event_loop so it runs
after the window adapter releases its borrow.
- Add "Go to album" to the TrackCard context menu and the list-row menu (adds
album-id to the feed model).
cc
- Fix the tray-restore wgpu crash (pre-existing, amplified by the app-wide
background): clear ImmersiveState.shader-texture on RenderingTeardown so the
first restored frame never hands a cross-instance stale texture to the new
femtovg canvas.
- Local/Plex feed covers now decode: route the "All" artwork jobs through
spawn_search_loads (http / local fs / Plex thumb) at all four dispatch sites;
show the source badge on the feed's album/track cards (AlbumCard maps plain
"qobuz" to the Qobuz logo).
- New card-sized LabelCard (200x246), used by the feed + the Favorites Labels
tab (replaces the round FavLabelCard).
- Artist pin badge updates live in the Favorites Artists grid: add is-pinned to
FavoriteArtistItem, stamp it, bind it in the ArtistGridCard, and flip the
favorites artist models in set_artist_row_pinned.
- Rich "All" list view: type-aware subtitle, quality badge (album/track),
source icon (local/Plex), favorite/follow indicator, and a per-kind context
menu (⋯ button + right-click).
cc
- All feed clicks were dead: the Albums/Artists/Playlists/Labels branch didn't
exclude the "all" tab, so an empty Flickable overlaid the grid and swallowed
every press. Exclude "all" from that branch.
- Local favorites never bound on the online session paths: init_for_user was
only called in the offline shell. Bind it in login + restore, teardown in
logout — so the heart flips and local items reach the "All" feed.
- Add the show-local source switch + the grid/list ViewToggle to the All
toolbar; implement the compact list view.
- ArtistPage "In library" rows rendered blank: add ArtistLibraryTrack/Album
artwork targets + dispatch cover jobs in navigate_artist.
- Move the catalog/library toggle into the artist header, floated right.
- Glass under the dynamic background: ToggleButton / ExpandableSearch /
SegmentedTabBar go translucent when app-background-active.
- Library > Artists grid now uses the ArtistGridCard (flat + A-Z grouped).
cc
- Library "All": mixed feed (tracks/albums/artists/playlists/labels) sorted by
date added, reusing the existing cards; new TrackCard; toolbar (search + source
switches + sort); "All" added to the Library nav surfaces.
- ArtistPage: restored the curated Playlists carousel (parsed but dropped in the
Slint port); added the catalog/library toggle backed by a per-session
per-artist index (favorites).
- Local-library favorites layer: new SQLite store (mirrors the pinned service),
album-level; excludes the Qobuz offline cache at write time. Feeds "All" behind
the show-local switch.
- i18n: 3 new strings across all 8 locales.
cc
The seekbar derived the absolute position from the local core's playback
duration, which is 0/stale while casting (the local backend is stopped),
so fraction * duration collapsed to ~0 and every drag restarted the
track. Resolve the real duration from the cast track's catalog metadata
instead (new seek_fraction_if_cast). Fixes seek on both Chromecast and
DLNA.
cc
A renderer left mid-playback by a previous session re-requests its
orphaned CurrentURI against the fresh media server (new path token ->
403 loop) and reports a stale PLAYING position. That poisons
cast_max_position and trips the premature-STOPPED end-detection guard
into auto-advancing the first cast track. Issue a best-effort AVTransport
Stop right after connect so the transport starts clean.
cc
getrandom::getrandom() was renamed to getrandom::fill() in 0.3; adopt fill() at the cast token call site. Consolidates onto the getrandom 0.4.2 that tempfile and rand already pull in.
cc
The 3 bundle-extraction attempts fired back-to-back, so a brief network hiccup
(DNS blip, dropped connection, captive-portal redirect) failed all of them in a
few ms and the retries were effectively useless — surfacing as "Failed to
extract bundle tokens: Network error" at sign-in. Add a short growing delay
(600ms, 1200ms) between attempts so a transient failure has time to clear.
cc
The metaball iso-surface + a strong high-frequency domain-warp octave produced
hard, clashing edges between the color lobes (visible over the translucent UI).
Gentle the high-freq warp (lower freq + amplitude), widen the iso-surface
smoothstep so lobes melt into the base with a long gradient instead of a hard
rim, ease the bright/dark contrast, and add a touch more dither against banding
— the flowing motion stays, the ugly edges go.
cc
- label.rs parse_top_track: the /label/page top_tracks carry the main artist in
a track-level `artists` array (roles=main-artist), not performer/artist (null
here), and the album title in `album.title`. Read both (with legacy fallbacks)
so label Popular Tracks show the artist name + album (discussion #631).
TopTrack gains an `album` field, threaded to the row.
- Artist + Label Popular Tracks: show-album: true so the album-title column
renders (owner: album required in all PopularTracks lists).
Note: "Add to Mixtape" is already present in the artist/label Popular-Tracks
multi-select bar (cassette icon, tooltip) and the per-track "..." menu
(mixtape-action = qobuz-row) — no code change needed there.
Base: 6c41f58f.
cc
NVIDIA shader fix (root-caused via diagnostic bisection on the discrete GPU):
- ambient.wgsl + plasma.wgsl: replace the sin-based hash `fract(sin(dot(p,k))*N)`
with the sin-free Dave Hoskins hash. On NVIDIA, as noise coords grow with
time, sin's argument gets huge and range-reduction precision diverges from
Intel's, degenerating the noise into big blocks. Confirmed via a solid-color
clear (clean) then a uniform-passthrough (clean) — isolating it to the shader
hash, not the texture handoff.
- ImmersiveView: the 3 immersive-exit paths (X button, in-menu exit, ESC) kept
the drain alive only for the Large NPB dock, freezing the shader texture on
the last immersive frame when the app-wide Ambient background was on. Now they
also keep it alive for Ambient so it renders the ambient scene after exit.
UI batch:
- Auto-hide scrollbar (Discover's ListScrollbar auto-hide + pointer hit-test)
on AlbumView, ArtistView, PlaylistView, PlaylistManagerView.
- #631: artist Popular Tracks now show the album cover thumbnail (thread
artwork_url through TrackData -> track_data_to_item + a new
ArtworkTarget::ArtistTopTrack decode job, mirroring LabelTopTrack); label
Popular Tracks fall back to the album's main artist for the name+id. Quality
badge already rendered by TrackRow — reused, not reinvented.
- #446: "Add to Mixtape/Collection" bulk button in the Favorites/Artist/Label/
Mix selection bars, mirroring the existing add-to-mixtape path (track items
auto-restrict the picker to mixtapes, not collections).
slintcheck + Opus Rust review green (no compile blockers). Base: 8c2f84b7.
cc
Smoke round 5 (owner feedback). Checkpoint.
- Preferred GPU selector (Settings > Renderer, Linux/wgpu): lists each detected
GPU by name ("Auto" + e.g. "NVIDIA … (discrete)" / "Intel … (integrated)"),
persisted (ui_prefs.gpu_power by device name), applied at startup via the wgpu
adapter PowerPreference (WGPU_POWER_PREF env still wins). Fixes the iGPU running
hot under the shader background on hybrid laptops — pick the discrete GPU.
New gpu_adapters() enumerates+dedups adapters; gpu_power_from_prefs /
_options / _seed_index / _value_for_index.
- Ambient metaball added to the IMMERSIVE scene picker (mode 7, wgpu-only opt-in);
the non-shader atmosphere stays the fallback for GL/software tiers.
- Glass treatment on the surfaces that were still opaque: carousel < > nav
buttons (6 carousels), "Filter by category" pill, "Filter by genre" buttons,
and the PlaylistBrowseView (View-all) header band + category radio row.
- i18n: "Preferred GPU" + description + discrete/integrated + restart toast (8
locales).
Base: 76ea5a82. Prior checkpoint: a0f5cf2b.
cc
Smoke rounds 3-4 (owner feedback). Checkpoint during iteration.
Ambient (the big one): rewrote ambient.wgsl from translating gaussian blobs to a
METABALL field (r²/d² summed + smoothstep iso-surface) with 2-octave domain warp
— lobes fuse/stretch/split like an amoeba / lava lamp (the Cider motion that was
missing), faster clock, more contrast.
Surfaces made background-aware (translucent when the dynamic background is on):
- Search results view (root + toolbar), artist Network panel + its tab-row
header (transparent to avoid double-translucent compounding), JUMP TO bar
(JumpNavBar bar-bg passed in — can't import ShellState without a cycle).
- Discover toolbars + tab pill + BrowseHeaderTools; SearchResultsView toolbar.
- CircleAction (shared: playlist/album/artist/etc. action buttons) on-surface
secondary fill goes glass; playlist Sort dropdown; QbzSelect closed control.
- New app-background-bar-alpha knob (0.3, env QBZ_BG_BAR_ALPHA): thin bars go
MORE transparent than panels (surface-alpha 0.5).
- Content well: frosted tint (0.22) + 1px hairline border → distinct panel.
- Album/Artist header gradient auto-suppressed under the dynamic background
(they clashed); header text stays light on the dynamic backdrop.
- Discover carousel edge-fade "shadows" hidden when active (Carousel /
PlaylistCarousel / ArtistCarousel / PinnedCarousel).
Base: 76ea5a82. Prior checkpoint: 4e405338.
cc
Smoke round 2 (owner feedback: content area was an opaque black slab, motion
imperceptible, ambient≈blurred).
- Content-area sweep: 27 content-view roots go transparent when the app
background is active (was opaque Theme.surface-main covering the background) —
Home/Discover/Album/Artist/Search-adjacent/Favorites/Playlist/LocalLibrary/
MyQBZ/managers/etc. Content well (AppShell) transparent too.
- SongCard (NPB classic), search input box, cortinilla panel go translucent so
they stop reading as opaque slabs over the background.
- ambient.wgsl: much faster + bigger blob travel + stronger domain warp +
brighter/more saturated so the flow is obviously moving and clearly distinct
from the blurred-art mode.
- Defaults: dim 0.35, surface-alpha 0.5.
Checkpoint during smoke iteration (owner-requested). Base: 76ea5a82.
cc
Two playback fixes:
- The 45s loading watchdog only cleared the spinner, so a load that hung
on a network stall (not errored) left the track dead at 0:00 with no
recovery — the legacy fallback only reacts to a setup *error*, never a
hang. Re-start the stuck track up to twice, then skip to the next
playable track. The counter is keyed per-track and resets the moment
real audio plays.
- NowPlayingState shuffle/repeat were only set on a manual toggle, so the
NPB Large shuffle indicator stayed dark after a session restore. Seed
both from persisted state in refresh_now_playing_meta.
cc
A use-site `changed has-focus` on a std-widgets LineEdit/TextEdit never
fires — has-focus is an alias into the inner TextInput (slint#7747) — so
typing a letter that doubles as a shortcut ran the action instead of
inserting the character. Sweep every text-input surface (search, import,
create/edit playlist & folder, tag editor, colour picker, settings
fields, QConnect dev modal, kiosk shell) and drive
UiFocusState.text-input-focused from a `guard-focused` probe property
that the change tracker does observe.
cc
The 2.0 port kept only the Odesli-by-URL path, which rejects Qobuz URLs
(400 could_not_resolve_entity), so album.link/Song.link never resolved
while the raw Qobuz link still copied fine. Restore the Tauri behaviour:
look the track up on Deezer by ISRC (album by UPC, with a leading-zero
retry) and build song.link/d/{id} / album.link/d/{id} from the match.
cc
Persist the Wayland app_id (set_xdg_app_id) and window geometry/maximized across
tray-hide and miniplayer round-trips so the pinned taskbar icon keeps grouping
and the size/position survive (#618); route the single-instance raise and MPRIS
Raise through a miniplayer-aware present() plus a dedicated D-Bus Present method.
Seed the Local Library Plex refresh button after login (not before, when the
per-user store is unbound). Also the shared wiring these features need in
main.rs / state.slint / app.slint / home.rs / artwork.rs (pinned globals + store
seed + mixed-artwork target, playlist-browse nav, sub-genre reload) and the
bundled i18n strings.
cc
Homologate the large-dock cover's side margins (16px each vs the visible content
bezel) and add a 4px bottom gap by trimming the FFT band 2px top/bottom. Move the
queue count out of the "Up Next" header into a fixed footer ("Showing X of Y
tracks", dropping the redundant "left"). On macOS, inset the flush-left NPB-small
art so the native rounded window corner stops clipping it.
cc
Enable the searchable QbzSelect on the theme picker (36 themes) — the search box
the migration deferred. Rides the #616 theme-filter (icon swap + list narrowing,
already on pre-release); the search filters within the active All/Dark/Light
facet.
cc
Users pin albums/artists/playlists to a configurable "Pinned" section on Home
and For You. New per-user SQLite pinned-items store (headless in qbz-app +
lifecycle wrapper), a mixed PinnedCarousel dispatching Album/ArtistGrid/Playlist
cards by kind, a rotated pin glyph (hover-only) on those cards, and Pin/Unpin
from the playlist header and the album/artist context menus. Local/Plex albums
resolve to their Local Library page (id-format routing) and render their real
covers via the mixed-source artwork dispatcher.
cc
New 200x246 artist card (framed round portrait, follow/play/more hover overlay,
centered wrap-capable name with an optional reco subtitle) matching the album/
playlist card footprint. ArtistCarousel renders it by default across the app
(search "Most popular" hero included); the legacy 160x220 ArtistCard stays for
the label/award carousels whose artist actions don't apply.
cc
Port the Tauri playlist "View all": a new PlaylistBrowseView with the category
tag bar, search filter, genre filter and grid/list toggle, extracted into a
shared BrowseHeaderTools and a reusable PlaylistListRow. Restores the "Popular
albums" see-all and aligns PlaylistCarousel's "View all" affordance with the
standard carousels.
cc
/discover/index honors sub-genre ids server-side, so send the raw selection
(parent or sub-genre, exactly as toggled) instead of mapping to the top-level
ancestor and narrowing client-side by genre.path — the previous hybrid widened
sub-genre selections back to their parent and dropped albums tagged only at top
level. Removes the client-side narrowing and its now-dead helpers; keeps a
diagnostic log of the outgoing genre_ids.
cc
Store the CMAF duration at command-accept so the seek bar and progress can't
freeze at 0:00, backfill it on the Resume rebuild, and clear + surface the
zombie stream-create-failure path. Degrade every non-priority device-reservation
contention error (only a higher-priority holder stays fatal) so a sandboxed
D-Bus RequestName denial can't block bit-perfect playback. Honor the
stream_buffer_seconds floor in the dynamic CMAF path and wire the network
throttle (record_segment_bandwidth / record_underrun / prefetch cap) to tame the
#591 underrun loop. Grant --own-name=org.freedesktop.ReserveDevice1.* in the
repo flatpak manifest so WirePlumber releases the card cleanly.
cc