JACK routes through the JACK graph (resamples to the graph rate), so it
can't be bit-perfect — but nothing told the user. Add a `backend-is-jack`
flag (settings.rs computes it beside alsa/pipewire at both set-sites;
state.slint property) and a WarningBanner in AudioSettings, gated on it,
pointing at ALSA-direct / PipeWire-passthrough for bit-perfect. UI-only;
backend behavior unchanged.
Blind change (no build this session) — validates on the next qbz_ui build.
cc
- About: contributors 4->5 per row so the 10 handles fill the widened
840px modal in 2 even rows instead of 3 sparse ones; modal height
720->770px.
- Splash: move the legal disclaimer out of the card to the bottom of the
screen (the main container, matching Tauri) and shrink it to 10px.
cc
Blocked albums looked like they "stayed" and clicking a card opened the
adjacent album: the model dropped the blocked album but the artwork jobs
were built from the UNFILTERED data with pre-filter indices, so every
cover landed on the shifted card (and the card's data was the next album).
- artist: artwork_jobs now skips blocked releases and uses a post-filter
album_idx that matches apply_artist's filtered+sorted model; the
"Latest release" is dropped at the source so the section hides and queues
no stale cover.
- favorites albums + home Recently Played: filter at the data source so the
model AND the artwork jobs derive from the same filtered list.
- albumview E1: append composers from the tracklist to the credit line —
album.artists carries only performers, so the composer (e.g. Mozart) was
missing; now derived distinct from track.composer with a Composer role.
cc
Phase 2 (Slint UI + wiring) for album-level blocking, plus the album-view
enhancements that share these files.
Album blacklist UI:
- "Block this album" in all three album menus: the page-header menu as a
Block/Unblock toggle (gated to Qobuz via AlbumState), and the AlbumCard +
AlbumListRow grid menus (one-way, drop-on-reload; local/Plex excluded).
Menu heights bumped for the extra item.
- Blacklist Manager gains an Artists | Albums tab bar (segmented, ADR-008);
an AlbumRow with a cover thumbnail (blind-eye fallback) + title + artist +
"Added {date}"; tab-aware controls (shared enable toggle, per-tab search /
Clear-All / count) and confirm modal. Settings row retitled "Blacklist",
status shows both counts.
- state.slint: BlacklistedAlbumItem + album-items/album-count/active-tab on
BlacklistState; set-tab/album-select/remove-album/clear-all-albums/
block-album on BlacklistActions; AlbumState.is-album-blocked.
- main.rs: ("album","block"/"unblock") arm (header, resolves display fields
from AlbumState); 5 album BlacklistActions callbacks; seed_blacklist_status
pushes album_count.
- blacklist_manager.rs: album build/push + set_tab/block_album/remove_album/
clear_all_albums; covers resolve async via a new ArtworkTarget::BlacklistAlbum
through spawn_loads.
Album-view enhancements (same files):
- E1: full credited-artist header line with localized roles, each name a
clickable artist link (Album.artists drove this; was interpreter-only).
- E3: classical work-header composer name is now a clickable artist link
(work title + composer id/name split through TrackData/TrackItem).
i18n: es (Mexican) for all new strings; de/fr/pt/ru/ja fall back to English
pending a translator pass.
cargo check -p qbz green (Slint codegen + bin).
cc
Phase 1 (headless). Adds the album-id filter axis on top of the artist
blacklist, orthogonal to it (an album is hidden by its OWN id regardless
of artist — the fix for Qobuz same-name artist merges).
- qbz-core: AlbumBlacklistFilter (HashSet<String>); album_blacklisted /
track_blacklisted / discover_album_blacklisted / pick_most_popular /
parse_search_all / search_all gain an album-id arm; fail-open guards
widened so an album-only block is not skipped. Artist arms (artist
search retain, pick_most_popular artists) stay artist-only. +6 tests.
- bridge: is_album_blacklisted / album_ids_snapshot / card_blacklisted;
stamp_row + is_track_blacklisted take the row's album id (source!="qobuz"
guard preserved; render/queue stay in lockstep).
- enforce at every album-bearing consumer, threading the album snapshot:
search (page-1 + load-more), home, discover, for-you (+spotlight),
album rows + queue + carousels, playlist/mix/favorites rows.
- close pre-existing leaks (both axes at once, owner decision E): label
pages (were entirely unfiltered — the prime merged-artist surface),
search load-more/replace-category, favorites albums grid, artist
discography (own-id), myqbz collection albums.
- albumview: full readable release date in the header ("Feb 19, 2026")
via the localized %b formatter, replacing the year-only slice.
cargo test -p qbz-app (12) + qbz-core (39) green; cargo check -p qbz green.
cc
Parallel album-id axis alongside the u64 artist blacklist. New
album_blacklist TEXT-PK table in the same artist_blacklist.db (additive,
idempotent, no migration), an in-memory HashSet<String> mirror, and
is_album_blacklisted/add_album/remove_album/get_all_albums/album_count/
clear_all_albums. Shares the single enabled flag with the artist axis.
12 unit tests green (incl. shared-flag + axis-independence).
cc
- Album view: classical "work" section grouping (PR #536 port) — new
Track.work model field + run-length per-work headers in AlbumView.
- i18n: add Russian + Japanese locales (1365 msgids each); extend plural.rs
(ru 3-form one/few/many, ja single-form) + tests; fix the language-selector
reseed (main.rs set_languages + ui_prefs index maps). Seven languages total.
- About modal: native port of the Tauri About (version, links, build info,
one-paragraph acknowledgments, inline contributors with GitHub avatars,
signature). Contributors credit hoyon (PR #536) and mxnix (PR #517, ru).
- What's New modal: renders the GitHub release-tag description (markdown
sections + TOC); release fetch + build.rs emits build date/commit.
- Discover Recommendations: move Cache window + Refresh now into the Customize
modal, gated to the reco tab; hide the section-config UI there; add an
explainer paragraph. New strings translated to all six non-English locales.
- About modal width +120px; splash screen gains the legal disclaimer footer.
cc
The sleep-timer PopupWindow used the default close-policy (close-on-click),
so any click inside it — including on the preset picker — closed it before
the value could change. Set close-policy: close-on-click-outside. Also
replaced the QbzSelect (itself a PopupWindow, which Slint cannot open nested
inside another PopupWindow) with inline preset rows, so the presets are
actually pickable.
cc
In multi-select mode the entire TrackRow body is a selection target (Tauri
whole-row selectable), not just the 13px checkbox. The body click routes the
same media-action("track", id, "toggle-select") the checkbox does, so the
central toggle arm reads the modifier — Shift+Click range and Ctrl+Click
work on the body too. The "..." context-menu button + the checkbox keep
their own handlers (the menu stays reachable); the other in-row controls
(artist/album link, favorite heart, offline button) are disabled in select
mode so the click passes through to the row. Double-click no longer plays in
select mode. Inert (blacklisted) rows never select.
cc
Extend the keyboard multi-select feature to the surfaces that had no
working selection in the Slint port.
- Mix (DailyQ/WeeklyQ/FavQ/TopQ all render through one MixView): mode
toggle + MultiSelectBar, per-row checkbox via the central toggle arm + a
ContentView::Mix branch (model + recount), bulk play-next/queue/
add-to-playlist. Shift-range, Ctrl+A and Escape inherited automatically.
- Label Popular Tracks: was a dead stub (checkboxes with no bar/recount/
router arms). Converted the local mode flag to LabelState.multi-select,
added a real MultiSelectBar + label.rs recount/select_all/clear/selected,
and the missing select-toggle/bulk router arms.
- LocalLibrary Albums grid (a previously-missing surface): AlbumCardItem
gains a `selected` field; a `select-mode` prop is threaded through the
shared AlbumCard / AlbumListRow / AlbumGrid / AlbumCollectionView (default
false, so Discover/Label/Favorites are untouched). Albums toolbar toggle
+ MultiSelectBar; local_library.rs album select fns (flat + grouped,
id-resilient Shift-range over AlbumCardItem); album->tracks bulk resolve
via fetch_album_tracks_blocking on spawn_blocking for queue/play-next/
add-to-playlist/add-to-mixtape; Ctrl+A and Escape routed by active-tab.
cargo check -p qbz green (qbz_ui compiles clean).
cc
Bring back the Tauri Excel-style keyboard multi-select to the Slint port:
Shift+Click extends an additive range from the last-clicked anchor,
Ctrl/Cmd+A selects all (select-all-only, never toggles to clear), and
Escape leaves select mode (clear + mode off). Plain/Ctrl+Click stay a
single per-row toggle (1:1 Tauri, which reads only shiftKey at click).
- New selection.rs: per-surface anchor (thread-local, id-resilient across
model re-sort/filter) + generic apply_shift_range and select_all over
any VecModel via a setter closure.
- keybindings::mods() made pub; Escape branch in handle_escape.
- Central toggle-select arm (Album/Artist/Playlist/Favorites) + dedicated
paths for LocalLibrary Tracks and Offline Manager (OfflineRow, range
setter guarded to kind==track).
- Ctrl/Cmd+A routed by NavState.view to the active surface's select-all,
gated on its multi-select flag; behind the existing text-input guard.
cc
Sleep timer (queue footer): tokio monotonic deadline + 1Hz countdown that
fires core().pause() at expiry; presets + custom minutes via a popover.
Stop after this song: 4 QbzCore passthroughs + playback consume-before-advance
seam and the load-bearing gapless-prefetch gate, plus the queue marker/menu so
the Queue persists when playback halts.
qbz-log crate: TeeLogger wrapping env_logger (stderr + 5000-line ring +
~/.local/share/qbz/logs/qbz.log) with write-time secret redaction (regex +
live-secret registry) and a diagnostics bundle builder.
Dev panel logs: in-app viewer that shows content the moment it opens, advanced
controls behind a discreet [+], "Copy all" as the sole footer button, a copy
button on the upload URL, and a redacted public paste upload.
Diagnostics: qbz-app::diagnostics (extracted, src-tauri untouched) feeding a
7-section panel with the live active output device + sample rate/format (pactl)
and a complete redacted report for the upload.
Report an Issue: wires the header menu item to a 1:1 Tauri modal that opens the
GitHub bug-report template.
Appearance persistence: 6 toggles that were live-only now persist via ui_prefs
+ handler arm + startup seed (window-title-show, show-volume-steppers,
sidebar-playlist-collage, local-library-track-artwork, in-app-toasts,
theme-filter).
Recommendations cache (per-tab only): TTL select 24/36/48/72h + Refresh now in
ExternalRecoView; no change to the other Discover tabs.
Bump workspace version 0.1.0 -> 1.2.15 so app_version is correct in the
diagnostics report and the QConnect About string. i18n es/de/fr/pt for all new
strings.
cc
A tray double-click fired two activate calls within milliseconds. On Wayland
each toggle destroys and recreates the winit surface and the wgpu shader
underlay, so a render still in flight then use-after-frees a TextureView
(wgpu-core panic "TextureView ... is no longer alive"). Collapse a double-click
into a single toggle with a 400ms debounce; the first click always passes.
cc
Circular 'liquid' audio-spectrum shader scene (Motion-Array style): FFT bands wrapped around a ring, smoothed into an organic blob glowing in the album palette, with a rotating radial-rays backdrop and a beat-triggered burst.
Wired as shader-mode 6 (liquid_spectrum.wgsl + SHADER_SOURCES[4] + render_frame), but the first cut did not land visually, so the picker row is commented out and the g-cycle is left at 0..5 — the scene is unreachable but kept intact for a future pass.
cc
Wave Bed (focus mode 6): centered vertical-mirror waveform. Spectral Ribbon (shader-mode 4): paint-as-you-play GPU spectrogram with green axes + header overlay, bass-at-bottom orientation aligned to the axes, and a real-time ceiling line tracking the highest active frequency. Line Bed (shader-mode 5): wgpu line-strip waterfall with a leveled camera, B-spline-smoothed continuous curves, a primary->accent height gradient, a bottom cutoff above the player, 2x supersampling, and a sampled-texture heights feed (no vertex storage buffer, so it runs on Slint's downlevel wgpu device).
Also: Plasma reworked to be far more reactive (4 ink emitters, beat splats, faster turbulence, quicker decay); immersive song card no longer truncates the album/artist line; player heart reflects favorite state; player bar narrowed; sample-rate and bit-depth wired through for the spectral header.
cc
Expand the shader Uniforms to 144 bytes (phase/beat/level/level_smooth + 8 log
bands + 5 energy bands + the album-art palette triad), feed them per frame from
the 30 fps visualizer drain (new FrameAudio + a forward-motion phase accumulator)
and push the palette on track change. Move plasma and aurora onto the new
uniforms and add a bottom-right ImmersiveSongCard shown over any shader scene.
Pairs with the faster tunnel from the previous commit.
cc
Amplify the forward-flight clock (phase * 6, seam-safe at the 4096 wrap), add
radial speed-lines fanning out of the vanishing point (shimmer with treble and
the beat), and bend the corridor CENTERLINE as a function of depth so the path
winds while the rectangular cross-section stays undeformed. Stronger beat punch.
tunnel.wgsl only; plasma and aurora untouched.
cc
The detail toggle used sidebar::contains to decide "following", which is
true for owned playlists shown in the sidebar and false for followed ones
opened from Favorites > Followed (a followed playlist may live in a folder
or not be rendered in the sidebar at all). Being in the sidebar is a
consequence of following, not the determinant, so derive it authoritatively
from get_user_playlists (present but owner != me). Reflect the state in the
button: a check icon + "Unfollow on Qobuz" tooltip when following.
cc
When the sidebar is fully collapsed, the header playlists flyout now has
a search box (sharing the sidebar's Rust-side filter, cleared on every
close so it never leaks into the expanded list) and an Import entry
(offline-gated, mirroring the sidebar "..." menu).
cc
Persist the source playlist id when a Qobuz playlist is copied (new
copied_playlists table in library.db, mirroring Tauri's user-scoped
qbz_copied_playlists set) and seed is-copied on detail-view load, so the
Copy button stays hidden on later opens, surviving an app restart.
cc
Classic NPB's "+" fired a one-click add-to-playlist; now it opens the same
grouped add menu (library / playlist / queue / play-next / mixtape / collection)
as New and Large. Classic keeps its inline favorite toggle next to it so the
favorite state stays visible.
cc
The Large now-playing cover sat flush against the window bottom. Lift it 2px and
take those 2px from the cover<->spectrum gap (10 -> 8) so the dock height is
unchanged; the bottom pad applies only while the spectrum is shown so the
off-state height still matches large-dock-height.
cc
A followed/subscribed Qobuz playlist could not be removed: the port flagged it
owned (via sidebar membership, which includes followed playlists) and called
delete_playlist, which Qobuz accepts but no-ops on a playlist you don't own.
- Ownership now follows Tauri: owner.id == current user id. Owned => Delete;
not-owned => the Follow/Unfollow toggle (previously hidden because the
playlist looked owned). Carry owner_id on PlaylistData; reset is-owner /
is-following / is-copied per navigation so state can't leak.
- The delete handler re-derives ownership server-side (get_playlist) as a
backstop for the manager/sidebar entry points and routes owned => delete,
followed => unsubscribe. delete_playlist never runs on a non-owned playlist.
- Post-removal nav only steps back in history when viewing that playlist's
detail; otherwise it stays on the invoking surface (sidebar refreshed),
instead of always routing to Favorites > Playlists.
cc
- Appearance: drop the red 'unsupported' tint on Language; hide UI Scale and
the Title Bar / Window Controls / Immersive blocks (commented, not deleted).
- Window title: "Show track in window title" now works -- the OS window title
becomes "{track} - {artist} | qbz" via a reactive binding (editable template
row hidden; format is fixed).
- Plex and Scrobblers collapse carets enlarged (28/18) and moved to row level so
they are vertically centered with the master toggle.
- Hamburger: "Open Qobuz Link" -> "Open Music Link".
- Link modal: a long pasted URL no longer grows the input/modal (min-width 0 on
the clip container caps content-width propagation).
cc
The floating back-to-top button overlapped the last track rows / the docked
Suggested Songs. Reserve the FAB footprint as a bottom footer so content
scrolls clear of it. Constant (not scroll-gated) to avoid a layout binding
loop (footer height -> track-list height -> visible-height -> FAB condition).
cc
- Radio Stations seed only Qobuz-sourced recent albums; local/Plex albums
carry ids the Qobuz /radio/album endpoint can't resolve, so they no longer
generate radio tiles (they still appear in Recently Played Albums).
- Recently Played albums/tracks route artwork by scheme (spawn_search_loads):
http -> Qobuz CDN, /library/ or /photo/ -> Plex thumb, else local file. The
plain HTTP loader left Plex/local covers blank.
- Radio Stations carousel extracted to a RadioCarousel component with prev/next
nav buttons (drag preserved), mirroring SlimCarousel.
cc
Discover > Recommendations Weekly Exploration/Jams kept vanishing and loaded
slowly. Three root causes:
- Weekly track resolution ran every track through MusicBrainz's serial
1.1s/req rate limiter (~110s for 100 tracks), so the rows never painted in
practice (Fresh Releases, album-based with no MusicBrainz, returned fine).
Resolve weekly tracks via fuzzy Qobuz search instead (skip_mb) -- fast and
reliable for these mainstream playlists.
- The external-reco cache was only initialized on the offline entry path;
the two online login paths (auth.rs) never bound it, so nothing cached
online (0-byte db). Initialize it there too.
- Weeklies were bundled in the shared 48h results blob keyed only on
auth-presence. Give them their own per-week cache (keyed by the
ListenBrainz playlist mbid, 9d TTL, written only when non-empty, with a
stale-fallback to the last good week) and rebuild them independently of the
blob, so a transient empty build cannot hide them. Skip the 7d negative
cache for weekly tracks so a throttle does not lock a row empty.
Per-row skeletons: the progressive paint now marks each expected row pending
at spawn and clears it as its builder resolves, so a slow row shows its own
skeleton instead of silently missing. Adds [reco] diagnostics.
cc
Owner-reported: Weekly Exploration/Jams showed once, then vanished on
restart. The 48h results cache stores the full ExternalCarousels; a
restart that triggers a fresh fetch with a transient ListenBrainz miss
(rate-limit / network / token-not-yet-restored) cached EMPTY weekly +
fresh-release rows, which the cache then served for the full 48h. Skip
the cache write when LB is connected but every LB-sourced row is empty,
so the next open re-fetches instead of serving the poisoned result.
cc
Owner feedback: the up-next must stay PAGINATED to bound CPU/rendering on
huge queues (1000+ tracks); the growing-list from the prior pass loads
them all. Restore PAGE_SIZE = 40. The cross-page drag (drop-on-page +
auto-scroll) is a separate pending rework.
cc
Was 6 albums each from 8 one-month top artists (yielding ~9 from 4 after
Qobuz validation). Take one album per artist (break) and seed from 50
lifetime top artists so the row spans >=20 distinct artists after
catalog validation.
cc
mark_artist_followed only updated the Search artist cards, so following
from (or affecting) For You / the Recommendations carousels left their
Follow chips stale. Flip `following` on all of them (Search results +
carousel, For You top-artists + artists-to-follow, reco rec-artists +
top-artists) so the chip hides ("auto") or flips to Following ("toggle")
wherever the artist appears.
cc
Two smoke fixes. (1) The Qobuz /radio/* endpoints return minimal track
objects (no performer/album), so radio-built queue rows showed no artist;
re-fetch full metadata by id via get_tracks_batch (order-preserving),
falling back to the originals. (2) After play_tracks rebuilds the queue
(album/radio/infinite-play refill), call sync_local_queue_if_changed so a
QConnect renderer pushes the new queue to peers immediately; it self-gates
to a no-op when not connected or a peer owns playback.
cc
The header heart binding + the Rust set_is_following calls reference an
ArtistState.is-following that did not exist (that property lived on other
header globals). Add it so the .slint compiles and the heart reflects the
follow state. Caught by cargo check -p qbz.
cc
Add the 10 new @tr labels (radio menu items, playlist Follow/Copy,
MusicBrainz setting) plus the new toasts to all five locale catalogs
(en/es/de/fr/pt); es is canonical Mexican, de/fr/pt machine-drafted and
flagged for review. Convert the new user-facing toasts in
enqueue_track_ids + the playlist-copy arm from raw strings to
qbz_i18n::t / qbz_i18n::tf so they localize like the rest.
cc
The ArtistView header heart dispatched a dead ("artist","","favorite")
with an empty id. Wire it to the artist follow (Qobuz favorite = follow)
as a real toggle that reflects state: bind the heart to
ArtistState.is-following, seed it on artist open, and toggle add/remove
via a new in-memory artist fav cache (fav_cache::is_artist_favorite /
set_all_artists / set_artist) seeded by search + the artist page. The
("artist","follow") arm becomes a toggle (was add-only), so the
search/discovery artist-card Follow chips can unfollow too. Optimistic
flip on the cache + every visible surface, revert on failure.
cc
with_db flattens the inner Result to Option<()> (so callers can
.unwrap_or_default()), so the favorite-revert guard must test ok.is_none()
rather than matches!(ok, Some(Ok(()))). Caught by cargo check -p qbz; the
whole bin now compiles clean.
cc
Add two title-adjacent icon buttons to the external-reco Weekly
Exploration and Weekly Jams rows: add the whole backing track list to the
queue, or create a playlist from it (reusing the add-to-playlist picker
from the queue save-as-playlist work, P6). SlimCarousel gains an opt-in
show-list-actions + list-action callback (default off, so the shared
recent/popular rows are unchanged); external_reco::list_track_ids reads
the row's backing ids; playback::enqueue_track_ids resolves + enqueues
them; a new ("ext-reco-list", _) media-action routes both.
cc
The queue sidebar paginated the up-next at 40 rows/page, which made
drag-reorder unable to move a track to a different page. Make the queue a
single scrollable growing list like the immersive + miniplayer queues by
having one "page" hold the whole up-next (PAGE_SIZE effectively
unbounded). The paginator auto-hides (page_count == 1), the list still
wheel-scrolls, and the drag slot now spans the whole list so a dragged
row reaches any position. Reorder/index math is unchanged (page stays 0).
Paginate unit tests made PAGE_SIZE-relative.
cc
MusicBrainz was hardcoded ON in the core client with no toggle, no
persistence and no startup wiring, so it could never be turned off. Add a
persisted opt-out (default ON: ui_prefs.musicbrainz_enabled +
Settings>Integrations toggle) that seeds the core client at startup and
flips it live via handle_bool.
Gate the MB-dependent surfaces when off: the artist Network/Scene sidebar
marks itself unavailable and opens on the MB-independent Magazine tab;
the playlist "Suggest songs" CTA hides and activate() early-outs; and the
musician page tolerates a disabled/failed MB resolve, falling through to
the Qobuz fallback instead of rendering blank.
cc
The heart on a Qobuz playlist detail was wired to Qobuz /favorite/create,
which does not accept playlist_ids, so it persisted nowhere and never
reflected in the library. Rewire it to the INTERNAL library favorite
(db.set_playlist_favorite, mirroring the Favorites card + Tauri's
v2_playlist_set_favorite) and seed it on open.
Add the two missing actions on a non-owned Qobuz playlist:
- Copy to library: clone it (create + add all tracks, carrying the source
artwork + attribution) as an internal owned copy.
- Follow on Qobuz: subscribe/unsubscribe via new qbz-core passthroughs to
the existing client subscribe_playlist/unsubscribe_playlist.
Surface hearted-but-not-owned playlists in Library>Playlists by merging
the internal favorite ids into the manager list (they don't come back
from get_user_playlists). Copies/follows already appear there (owned /
subscribed). New PlaylistState.is-following / is-copied.
cc
When the queue runs out with InfiniteRadio autoplay on, build a smart
artist radio seeded by the just-finished track and start it, so playback
continues with related music and chains radio -> radio. The toggle, the
AutoplayMode preference and the radio primitives already existed; only
the trigger and the immersive button were missing.
Wire the trigger at the auto-advance queue-end (naturally peer-gated) and
enable the immersive infinity button (binds QueueState.infinite-play +
toggle-infinite-play). Add QueueController::is_infinite_play().
Tauri appends the radio and retries next(); that would replay the old
queue here because QueueManager::next() nulls current_index at the edge,
so this starts the radio fresh via play_tracks to reach the same intended
behavior. Manual-next refill is deferred (needs an explicit peer gate).
cc
The queue footer button fired a correctly-wired callback whose handler
was a stub that only logged "modal pending". Replace it with the faithful
Tauri behavior: resolve the queue's tracks (current + upcoming,
de-duplicated) and open the add-to-playlist picker seeded with them, so
the inline "Create new playlist" row turns the queue into a named
playlist and picking an existing one appends to it.
Add a reusable playlist_picker::open_for_ids(window, runtime, handle,
ids, local) that dedups the open_multi + load + apply idiom; it is also
the entry point the reco-row buttons (P7) reuse.
cc
Qobuz /radio/album and /radio/track return a tracks page that omits
`total`; the strict SearchResultsPage<Track> derive rejected it with
"missing field `total`", breaking AlbumView and Discover For You radio.
Port Tauri's lenient_page_flexible into qbz-models and apply it to
RadioResponse.tracks (now non-optional, matching Tauri); derive Default
on Track so the absent-tracks branch resolves to an empty page. Two new
qbz-models tests cover the missing-total and absent-tracks cases.
Wire ArtistView's previously-dead radio dropdown: both items now carry
ArtistState.id and have handler arms -- "QBZ Radio" -> smart pool
builder, "Qobuz Radio" -> the plain /radio/artist endpoint
(play_artist_radio, no longer dead_code). Add a "QBZ track radio"
track-menu item backed by a new play_smart_track_radio that fetches the
track to seed its performer.
cc
Surface the new Recommendations tab everywhere Discover sub-sections are listed,
and make it opt-out.
- Flyouts: the sidebar section-nav flyout and both header nav flyouts (full text
+ compact icon) now list "Recommendations" alongside Home / Editor's Picks /
For You. The Rust route handler already maps "discover-recommendations"
(strip_prefix + select_tab + ensure_loaded), so no routing change was needed.
- Opt-out: a show_recommendations flag (default on) persisted in the per-user
discover-prefs store (qbz_app), seeded into SettingsState on session start.
Settings > Integrations gains a "Show Recommendations in Discover" toggle whose
description nudges connecting Last.fm + ListenBrainz (MusicBrainz is used
automatically to match releases). Off => the tab pill and every flyout entry
disappear.
- Gating uses two mounted instances per flyout (one with the entry, one without)
because Slint can't type-infer [NavMenuEntry] from a ternary of array literals;
the HomeView pill uses a plain if.
cargo check -p qbz green.
cc
Owner-directed polish after testing the Recommendations tab.
- Full albums only: drop singles/EPs/boxsets/compilations from every album row
(Recommended Albums, Fresh Releases, Deep cuts). Qobuz's `release_type` is the
source of truth ("album" | "single" | "boxset" | "compilation"); when absent,
fall back to tracks_count >= 5. Kills the karaoke single that showed up under
Fresh Releases.
- Slop second layer: reject karaoke / tribute / "made famous by" / "in the style
of" / instrumental-version releases by artist-or-title keyword, applied both
pre-resolution (candidates) and post-resolution (the Qobuz album), so AI-slop
that still wears a full-album shape gets filtered.
- Taller rows in this view only: AlbumCard/ArtistCard/Carousel/ArtistCarousel
grow an overridable `card-height` (defaults unchanged, so Home/Search are
untouched); ExternalRecoView passes 266 to album rows and 256 to artist rows,
whose cards carry the "Similar to…" subtitle (~250px) and were clipping the
Follow chip at the old fixed 220.
cargo check -p qbz green.
cc
Owner-directed polish on the Recommendations tab to make it richer + faster +
non-repetitive.
- Common vs Recent split: the single "Recommended Artists" row over-represented a
recent soundtrack binge (a live preview showed "The Newton Brothers" flooding
the row with 8 film composers while the core metal was buried). Split into two
rows seeded from DIFFERENT periods: "More like the artists you love" (overall
top) and "Based on what you've been into lately" (1-month top) — so recent
one-offs never pollute the core taste. = 7 rows now.
- Round-robin per seed inside each artist row (re-added from the v1 engine; the
rework had dropped it) + per-seed cap, so no single seed floods the carousel.
- Results cache (48h TTL): caches the FULL built rows (serialized
ExternalCarousels) per-user, keyed by connected sources. Tab open within 48h =
INSTANT paint from cache, zero fetch; stale/missing = build + store. Rebuilds
every 48h (fresh rotation) so it's never "eternally the same" and opens stop
being slow. Distinct from the rec->Qobuz resolution cache (30d/7d).
- Toast on a real fetch (cache miss/stale): "Generating recommendations…" so the
wait is communicated; the skeleton stays; no toast on a cache hit.
cargo check -p qbz green (6m44s).
cc
Reworks the 4th Discover tab per owner feedback (the track-heavy v1 surfaced
already-heard artists). Verified against three sources that Last.fm has NO
recommendations API (getRecommendedArtists removed; web is PJAX HTML behind the
website cookie, which the API session key cannot reach) — so recommendations are
replicated from the documented similarity recipe, and ListenBrainz provides the
genuinely clean personalized feeds.
New lineup (each row paints progressively as its builder resolves):
- Recommended Artists (Last.fm artist.getSimilar seeded by last-month top, minus
the heard set; "Similar to X, Y" subtitle on the artist card).
- Recommended Albums (Last.fm artist.getTopAlbums minus your scrobbled albums).
- Fresh Releases (ListenBrainz, from artists you follow).
- Weekly Exploration + Weekly Jams (ListenBrainz curated playlists, by source_patch).
- Deep-cut albums from artists you know (Qobuz catalog, not heard in-app).
- Cold-start fallback: Qobuz editorial top albums + artists.
The "not heard" filter is now ARTIST-level (the v1 bug derived artists from
unheard TRACKS) and compares against the LOCAL reco history + a light Last.fm/LB
top-set, not a full history sweep. Album validation (UPC -> fuzzy text) added.
qbz-integrations gains Last.fm get_artist_top_albums/get_user_top_albums and
ListenBrainz get_created_for_playlists/get_playlist_tracks/get_fresh_releases.
NOTE: the Cloudflare proxy was switched to allow ALL Last.fm read methods
(anything with ".get"), so future reads need NO proxy redeploy — but the live
proxy must be redeployed ONCE with this change or all Last.fm reads keep 400ing.
cargo check -p qbz green.
cc
Adds "Recommendations" as a 4th Discover tab (alongside Home / Editor's Picks /
For You), rendering the qbz-external-reco engine output. NOT part of For You.
- ui: new ExternalRecoState global + ExternalRecoView (4 personalized carousels
C1-C4 with length>0 self-hide gates + the editorial cold-start rows), 4th tab
pill button in HomeView, mount on active-tab == "recommendations", skeleton
gate. Export the global from app.slint.
- bin: external_reco controller — RecoCatalog over QbzCore, per-user resolution
cache lifecycle, LocalHistory from reco_store (known artists + recent plays),
scrobbler-username gate (is_authed), build + apply into ExternalRecoState
(models built on the UI thread; slint::Image is !Send) + artwork jobs.
- bin: 6 ArtworkTarget variants + resolution arms; reco::recent_track_ids
wrapper; home::select_tab + discover_prefs::push_descriptors treat the new tab
like For You (own view, Home/Editor pushed empty); lazy-load hooks at the tab
click + both Discover nav-restore sites.
cargo check -p qbz green. Last.fm reads still need the proxy allowedMethods
deploy (separate qbz-api-proxy repo) before they return data.
cc
New frontend-agnostic crate qbz-external-reco: blends Last.fm + ListenBrainz
recommendation signals into the Discover "External Recommendations" carousels,
validating every candidate against the Qobuz catalog (ISRC direct -> MusicBrainz
inc=isrcs bridge -> fuzzy text scorer, gated) before display. Four carousels
(C1 similar artists / C2 similar tracks you haven't heard, C3 listened-but-not-
recently, C4 deep cuts from known artists) capped at 20 with daily rotation over
a larger cache-backed pool, plus a Qobuz-editorial cold-start fallback (top
albums + artists) when no external source is connected. Round-robin blend across
sources. Resolution cache (per-user SQLite, 30d hits / 7d misses, WAL).
Supporting public read methods added to qbz-integrations:
- Last.fm: get_top_artists / get_top_tracks / get_loved_tracks /
get_recent_tracks / get_similar_tracks (public reads via the Cloudflare proxy).
- ListenBrainz: get_cf_recommendations / get_recent_listens /
get_metadata_recordings (204/404 -> empty, never error).
- MusicBrainz: get_recording_isrcs (the MBID -> ISRC bridge).
Engine is headless and unit-tested (matching + cache); no qbz-ui dependency.
cc