Settings > Audio gains a 'DSD playback' dropdown (Convert to PCM /
DoP / Native DSD, ALSA-direct only, permanent warning copy since DoP
and native support cannot be detected). Local .dsf/.dff files route to
the player's DSD path with toasts on unsupported files. Quality badges
read DSD64/128/256 from the stored bit rate on every surface including
the now-playing stamp. The gapless prefetch now pre-queues LOCAL
tracks too: DSD via the DoP/native append or an in-memory converted
WAV, other local formats via their raw bytes (fixes local FLAC
gapless); CUE virtual tracks are excluded. Six new locale strings.
cc
Conversion rides the existing play_streaming path as an exact-size
88.2 kHz/24-bit WAV. DoP and native DSD (Phases 2-3) get a dedicated
direct engine: additive S32/DSD_U32 ALSA stream constructors and a
verbatim i32 write path (no f32, no gain), a writer thread that feeds
0x69 DSD silence on pause and pads stream teardown, and a source queue
for gapless DSD. AudioSettings gains dsd_mode (convert|dop|native,
default convert). Delivery resolves per track: user mode + ALSA direct
backend + stereo + rate support, everything else converts. Seek is
ignored in direct modes and the pause suspend path is gated off there.
cc
Scanner accepts .dsf/.dff; new AudioFormat::Dsd; metadata extraction
via qbz-dsd (embedded ID3, artwork, bit_depth=1 with the DSD bit rate
as sample_rate so badges derive DSD64/128/256). Ephemeral folder open
now sorts tracks by album/disc/track/title and re-keys ids so the
displayed order matches the play-queue order (was readdir order).
cc
New frontend-agnostic crate for DSD support. In-house DSF and DFF
demuxers (up to 5.1, DST rejected with a typed error), embedded ID3
tags and artwork via the id3 crate, a verbatim BSD port of Sebastian
Gesemann's dsd2pcm table engine plus a half-band decimation chain
(uniform 88.2 kHz output, ITU stereo downmix for multichannel), DoP
frame packing per the DoP 1.1 standard (marker alternation, 0x69
silence), and native DSD_U32 packing (BE/LE) for kernel-granted
devices. 20 tests over synthesized fixtures.
cc
Custom title bar (Linux opt-in, macOS overlay default): frameless window
with drawn window controls (close/maximize/minimize, left or right
placement), header drag with once-per-press latch (Wayland move-grab
swallows the release; re-arming on stale pressed state re-grabbed the
window onto the cursor), double-click maximize, edge resize, 1px
hairline. Title bar minimize is always a WM minimize now - the old
minimize-to-tray branch made it visually identical to close-to-tray.
Compact header navigation (opt-in, Appearance): with nav out of the
sidebar, the header can always use the icon-only compact nav even while
the sidebar is open. Full text tabs and compact nav stay mutually
exclusive; the playlists flyout button keeps its closed-sidebar-only
behavior. Persisted as ui_prefs.nav_header_compact.
i18n: new strings in all six locales (includes two DSD toast strings
for the in-progress DSD support, riding along in the catalogs).
cc
Zero missing msgids in all six locales (was 27 es / 36 de-fr-pt / 50
ru-ja): renderer tiers, shortcut modals, link resolver, Purchases, the
album-blacklist batch (de/fr/pt/ru/ja) and the recommendations + JACK
batch (ru/ja); plus es Collections leak fixes. msgfmt-validated.
cc
A standalone repro mirroring the exact chain (if-mounted view, Flickable,
layouts, if/for components, nested slot) measured absolute-position as
exact window coordinates at any scroll position in Slint 1.16.1 — the
double-count quirk is ListView-only and this chain has none. The anchor
math is correct as written; no code change.
cc
Idle timer at the Cortinilla root (a Timer inside the if subtree cannot
be restart()ed — 1.16 compiler panic): paused while the pointer is over
the panel (ancestor TouchArea hover covers the row/button children),
restarted by typing and arrow-selection moves, hover mirror reset on
open/close.
cc
The search inputs' changed has-focus handlers re-invoked live() whenever
focus returned with text present, and Slint hands focus back without user
intent (focus reassignment when the focused element unmounts on a page
change, window re-activation) — the dropdown popped open on navigation.
Gate the refresh on the dropdown being already open; typing still opens
it through edited. Also release the header search focus and reset the
typing guard on every view change, so single-key hotkeys work again after
typing a search, and a guard left stuck true by an unmounted input is
cleared.
cc
The single-key hotkey guard (UiFocusState.text-input-focused) was missing
on the three editable TextEdit description fields (create/edit playlist,
My QBZ edit) and on the read-only inputs (QConnect dev modal, log viewer
URL), so Space/q/? hotkeys fired while typing or selecting there.
cc
fontique's CoreText-based Han-script fallback fails to resolve a family on
modern macOS (the suggested PingFang is not always in the scanned family
map), so Japanese kanji rendered blank while kana worked. Vendor
i-slint-common 1.16.1 and pre-register Hiragino / PingFang / Apple SD
Gothic Neo / Arial Unicode MS in the fallback map at collection creation
(Japanese-biased default; the map takes priority over the CoreText query).
macOS-only cfg; Linux keeps its working fontconfig fallback. Type-checked
against aarch64-apple-darwin.
cc
Flatpak manifest builds the crates/ workspace with rust-stable+llvm20
(node SDK and the intltool/libdbusmenu/ayatana chain removed). Root
snapcraft.yaml becomes a from-source Slint recipe (core24, rustup,
MIN memory tier) mirroring packaging/snap-slint runtime sections.
AUR/arch/gentoo swap webkit/gtk/appindicator runtime deps for the
measured Slint set and the source builders compile crates/ directly.
flake.nix: cargoRoot crates, no npm hooks, dlopen runtime libs wrapped.
cc
release-linux, release-linux-aarch64, release-macos and release-flatpak
now build the crates/ workspace (Tauri npm builds removed) with the
proven build-slint recipe: 18 GB extra swap, mold, pinned nightly, MIN
memory tier. Build-once-package-many: nfpm deb/rpm, minimal-AppDir
AppImage, and the AUR/Gentoo/APT tarball with its name and layout
contract preserved. macOS assembles a QBZ.app skeleton, ad-hoc signs
it, and packs a dmg with hdiutil.
Cross-cutting: prerelease detection extended to -rc/-beta/-alpha/-pre,
tag-on-main ancestry guard (manual dispatch skips it), fail-fast
version assert against crates/Cargo.toml, unstripped binary archived
per tag. Tauri-updater bridge: AppImages and macOS .app.tar.gz are
signed with the existing Tauri minisign key so 1.2.x installs
auto-update into v2 via the unchanged release-updater-manifest.yml.
cc
Icons and qbz.desktop move to packaging/ so packagers stop depending
on the retired src-tauri tree. New nfpm.yaml builds deb/rpm from the
prebuilt binary: deps are the measured ldd set (t64 alternatives on
deb, soname requires on rpm), display/GPU stack as recommends.
cc
The release binary carried 173 MB of symbol tables (.strtab 151 MB +
.symtab 22 MB, no .debug_* sections) inflated by Slint's generated
generic names: measured 400 MB -> 218 MB (-45%). Panic backtraces
become address-only; the release workflows archive the unstripped
binary as an artifact per tag for offline symbolication.
cc
The screen sat inert while the system-browser OAuth ran: no waiting or
authenticating feedback, and failures were log-only. New LoginState
global (phase + error) driven by an on_phase callback in
auth::login_via_system_browser (both call sites: login + recovery).
The blue button now reads "Sign in with your browser", disables while
an attempt is in flight, and the screen shows continue-in-browser text
(with a no-focus hint), a Cancel link that aborts the pending flow, an
authenticating state, and a sign-in-failed box. The redundant
"Use your system browser instead" link and its AppCommand are removed.
Five new msgids translated in all seven locale catalogs.
cc
The six local SecondaryButton/ManageButton copies in the settings panels
and the two inline AudioSettings buttons had fixed widths sized for
English; Spanish/French/German labels overflowed the borders. Width is
now Math.max(label preferred-width + padding, old floor), so short
labels keep the 160/170/200px right-edge grid and long translations
grow leftward instead of overflowing.
cc
Webview-zoom-style presets: Extra small 0.8 / Small 0.9 / Default 1.0 /
Large 1.2 / Extra large 1.5, applied through SLINT_SCALE_FACTOR set at
the top of main() before any thread or the backend exists. The env var
overrides the compositor DPR, so the value is last observed DPR x
preset; the Default preset leaves the var unset (stock DPR handling,
including live monitor changes, stays intact). last_dpr is persisted
from the winit window (real compositor DPR) on the 5s renderer-sentinel
timer.
- const-bake guard: strip SLINT_SCALE_FACTOR in qbz-ui build.rs so the
slint compiler never bakes the factor into the generated code
- new UiScale global + scale-divided window minimums keep the physical
minimum constant across presets (XL fits a 1366x768 screen); the
three window-geometry gates in main.rs use the process-active factor
- artwork decode targets scale with the preset: sharp covers at >=1.2,
smaller decoded-cache footprint below 1.0
- Settings > Appearance "Interface size" row below Language (replaces
the commented-out percentage placeholder and its orphan properties),
restart-to-apply toast
- Typography.boost = 1.1 on Extra small only, so body text stays
readable at the 0.8 factor; Small (0.9) needs no compensation
- i18n: new strings added to all 7 locale catalogs
Known accepted limitation: slight text softness at fractional scale
factors (owner-accepted; diagnostic notes live in
qbz-nix-docs/ui-scale-presets/).
cc
On weak GPUs every animation frame is a full-window femtovg repaint, so
continuously-animated loading indicators dominate transient CPU (opening
the search cortinilla spiked to ~40% on a 2014 Mac Mini). New
ShellState.reduce-motion (seeded true on non-wgpu tiers, like the shader
gate) switches those animations from display-rate animation-tick to a
shared ~8fps coarse clock advanced by ONE timer in AppShell — same phase
math, 60 -> 8 repaints per second:
- LoadingSpinner / LoadingDots (all mount sites inherit)
- Equalizer bars (TrackPlayCell + immersive track info)
- Fetch/download spinners (TrackPlayCell x2, TrackRow, offline manager)
- Both search-cortinilla skeleton pulses (the reported spike)
- Home skeleton: the 900ms opacity fade collapses to a snap (its phase
timer becomes the only driver, ~1fps while mounted)
Also logs the window scale factor at startup (circle-AA investigation:
femtovg's fringe AA halves at fractional scale factors, dpi=ceil(scale)).
cc
Covers and card elements now scale with the viewport instead of the
library: the shared AlbumGrid gains opt-in windowing (row band = visible
rows +/- one viewport, computed in .slint, row-granular change events),
cards outside the band render as same-footprint placeholders (scroll
geometry unchanged), and the Rust side dispatches artwork only for the
band, evicting covers outside a retention span back to the placeholder.
- New id-keyed ArtworkTarget::LocalAlbumById (replaces the index-keyed
LocalAlbumCard): immune to derive re-sorts between dispatch and apply;
reloads still drop stale covers via the generation guard. The enum
loses Copy (String variant).
- In-flight dedupe set freed on apply AND on fetch failure, cleared on
generation bumps.
- Flat list view and grouped mode keep the pre-windowing full dispatch
(they don't fire window events); switching grid->list re-dispatches
covers the window may have evicted.
- Windowing is off by default in the shared grid: Discover, Labels and
Favorites are untouched (phase 2).
A 2,000-album library drops from ~540 MB of resident cover RGBA to a
viewport-sized working set, and scrolling repaints ~40 mounted cards
instead of the whole collection.
cc
The immersive shader scenes (Plasma/Tunnel/Aurora/Spectral Ribbon/Line
Bed) render through the wgpu underlay, which does not exist on the
femtovg-GL and software tiers - picking one there showed a black view.
Gate the five picker rows and the g-cycle behind a new
ImmersiveState.shader-scenes-available flag seeded from the renderer
selection; FOCUS views and splits (Slint-drawn) stay available.
cc
Weak-GPU hardware (Raspberry Pi 4 / v3dv, Haswell / hasvk) exposes a
real Vulkan adapter, so the old binary wgpu-or-software detection handed
it the slowest possible path: femtovg-wgpu over an immature Vulkan
driver. Mesa's GL path is the fast one on that class of hardware.
- Three-tier chain: wgpu -> femtovg GL -> software. Auto-detect
classifies adapters (weak-driver markers incl. hasvk, integrated-on-arm,
tiny texture limits); adapters only reachable through wgpu's GL backend
never select the wgpu tier (the renderer masks Backends::GL out, so
detection must too). renderer-femtovg (GL) feature now compiled on
Linux.
- wgpu tier requests PowerPreference::LowPower (WGPU_POWER_PREF still
wins).
- Per-window swapchain alpha in the vendored femtovg renderer: only the
miniplayer gets a transparent (blended) swapchain; the main window
keeps an Opaque one, sparing a full-window compositor blend per frame.
- Settings > Appearance gains a Renderer row (Linux only): Auto
(default) / GPU / GPU compatibility / Software, persisted in
ui_prefs.json, applied on restart. QBZ_RENDERER env still overrides
(now also accepts gl|gles|femtovg).
- Auto-revert sentinel: a non-auto choice is armed before backend init
and disarmed after the window survives 5s; if the previous start died
before that, the setting reverts to Auto with an explanatory toast, so
a bad renderer pick can never lock the user out of Settings.
- Developer tools > Diagnostics now reports the active renderer, why it
was chosen, and the enumerated GPU adapters (panel + markdown report).
cc
The atmosphere mount in ImmersiveView kept the default animated:true, so
the 4-layer warped background forced full-window redraws at display rate
even with playback paused. Gate it on NowPlayingState.playing (the other
three mount sites already pass animated:false).
cc
- DECODED_PIXEL_CACHE is now byte-budgeted (48 MB, 24 MB on <8 GB RAM
hosts) instead of a 256-entry cap that could silently hold ~370 MB of
1000px now-playing decodes.
- Route the full-resolution Image::load_from_path callsites (offline
manager/favorites, local library ephemeral albums, MyQBZ, playlist
covers) through the existing decode-to-display-size pipeline.
- Cap Home playlist cards at 40 (was 100).
cc
- The 450ms poll pushed ~10 NowPlayingState properties every tick even
with nothing playing; Slint properties have no equality guard, so this
forced a full-window repaint ~2.2x/s forever. Snapshot-compare the
pushed values and skip the event-loop hop when nothing changed (poll
cadence untouched - QConnect reporting and gapless detection depend
on it).
- Palette/atmosphere extraction ran on the UI thread with four full
pixel-buffer copies per track change; do one shared downscale off the
UI thread and only set the results in the event loop.
cc
- Gate the shader drain on the immersive view being OPEN, not just
shader-mode > 0: the three close handlers never reset shader-mode, so
a picked shader scene kept rendering a full-res pass at 30fps forever
after closing the immersive view.
- Start/stop the drain timer from set-enabled instead of running it 30x/s
for the app lifetime; park the FFT producer thread on a condvar while
the tap is disabled.
- Compute the 5 energy bands in one spectrum pass (was two identical
scans) and reuse the waveform buffer.
- Shader underlay: rotate a small pool of render targets instead of
creating a fresh 2560x1440 texture every frame (~440 MB/s alloc churn),
clamp the target to the window physical size, and build pipelines
lazily on first use instead of compiling all six WGSL shaders at
startup.
cc
glibc 2.43 re-versioned six libm float functions (sinhf, coshf, atan2f,
acosf, asinf, acoshf); a binary linked on such a host requires
GLIBC_2.43 and refuses to start on older distros. Wrap each symbol via
-Wl,--wrap to an f64-based shim in glibc_compat.rs so the binary only
needs the ancient symbol versions (runs on glibc >= 2.39).
cc
--memory-swap=26g OOM-killed the ~30-33 GB qbz_ui rustc. Bump to 24g RAM /
48g total — above the demand, still well under the host's 30G+46G so a
runaway is caught without freezing the box.
cc
qbz-ui's Slint build imports the lyrics fonts from the repo root
(../../../../static/fonts/*.ttf), which is outside the crates/ workspace
cross mounts — so the cross build couldn't find them. Mount static/ at its
host path via CROSS_CONTAINER_OPTS.
cc
cross 0.2.5's default aarch64 image is Ubuntu 16.04 (xenial): libfreetype-dev
doesn't exist there (it's libfreetype6-dev) and its OpenSSL 1.0.2 / archived
repos break modern deps. Pin the :main image (Ubuntu 20.04+) and use the
libfreetype6-dev name (valid on both).
cc
Native (on an ARM runner: apt deps + cargo build -p qbz) and cross (from
x86-64 via cross + Cross.toml arm64 sysroot) paths, auto-selected by host
arch. Documents the ~30 GB qbz_ui RAM wall (8 GB Macs / 4 GB Pis cannot build
natively). Cross path is best-effort/untested pending a first run.
cc
The thin Unicode arrows (left/right/up/down) were nearly headless at keycap
size even after the font bump, so the arrow-key shortcuts use solid triangle
glyphs now. The footer Reset All to Defaults icon rode high next to its label
(a bare fixed-size icon in the row) — wrapped in a centered column.
cc
The hamburger "Documentation" entry had no clicked handler, so it did
nothing. Point it at github.com/vicrodh/qbz/wiki via the generic
external-link handler.
cc
The editable Customize Shortcuts modal now lays its categories across three
columns (extracted KbEditGroup, reusing the cheatsheet's column models) so it
matches the cheatsheet and avoids a tall scroll; panel widened to fit. Keycap
text bumped to 15px in both modals so the arrow-key glyphs read clearly.
cc
- Acknowledgments now lead with Rust (the app's core, previously unmentioned)
and drop Kawarp, which is no longer used (es/de/fr/pt updated).
- Logo renders from a 128px source instead of the 512px original scaled to
56px, which aliased badly (femtovg has no mipmaps).
- Qobuz trademark notice dropped to 11px so it wraps to two lines, not three.
cc
The window-level hotkey handler only suppressed single-key shortcuts
(Space/q/?/1-5) when UiFocusState.text-input-focused was set. The ~24
std-widget LineEdit fields across 14 modals/settings never set it, so those
keys fired (and were swallowed) mid-typing. Each now mirrors its focus via
`changed has-focus`; import added where missing.
cc
The read-only shortcuts modal listed every category in one tall column that
scrolled. Split the groups round-robin into three column models (Rust) and
render them side by side; the panel widened to fit. The editable customize
editor still uses the full single-column list.
cc
"Where you left off" only restored five id-free top-level surfaces, so
browsing Discover or any album/artist always reopened on Home. Persist the
full nav destination (serde-encoded NavEntry in ui_prefs.last_nav) and
restore it via apply_entry — album/artist/playlist/mix/label/etc. re-fetched
by id. Online restores the exact view; offline keeps the local top-level
fallback; Search/Settings are not persisted; Home stays the safe root.
cc
The now-playing bar, MPRIS metadata, and the desktop notification showed
the clean album name; for a re-edition (e.g. Octavarium (2009 Remaster))
the variant was lost. Carry the album-level version on QueueTrack
(populated on the album-play path) and append it to the displayed album.
Last.fm scrobbling keeps the CLEAN album name so editions don't fragment
stats. New album_version field defaults to None on every other queue source.
cc
Albums with a distinct version (remaster, Hi-Res re-edition, deluxe, etc.)
rendered as identical duplicates because the version was dropped. The web
player appends it in parens; mirror that via format_album_title across the
discography (map_release), search (map_album), the album header (map_album),
and the shared card mapper (suggestions/label/favorites). Verified against
live API: some variants carry the suffix in title (version null), others in
the version field — appending only when present matches the web player with
no double suffix. Add the version field to the Album model.
cc
Second album-page row sourced from Last.fm: the album's primary artist ->
artist.getSimilar -> one top album per similar artist, resolved to Qobuz and
deduped against the Qobuz /album/suggest row (by artist|title and by id).
Gated on Last.fm being connected; resolved row cached per album for 30 days
(re-opens make zero API calls; only non-empty results cached so a transient
failure re-fetches). New build_similar_albums_seeded in qbz-external-reco.
reset_album now also clears this row so the previous album's recommendations
don't linger while the next loads.
cc
The album header (⋯) and cover-art PopupWindows used the default
close-on-click policy, which races the row's TouchArea — the popup can
dismiss on the press before the item's clicked fires, so some rows (e.g.
Share Qobuz link) needed a second open+click. Switch both to
close-on-click-outside and close explicitly per row, matching the existing
Sidebar/QueueSidebar fix.
cc
/album/suggest returns a page with only {limit, items} (no total or
offset). SearchResultsPage required them, so the whole AlbumSuggestResponse
failed to deserialize and the album Suggestions carousel silently never
showed. Default the pagination scalars so only items is required.
cc
The header credit line derived composers by scanning every track's
composer, which over-credited songwriters on non-classical albums
(e.g. Anthrax showed Spitz/Bello/Benante). The official web player
instead uses the single album-level composer field and suppresses the
Various Composers placeholder.
Mirror releaseArtistsMapper: append one album.composer, skipping it
when the name contains VARIOUS (case-insensitive) or the id is already
credited. Add the composer field to the Album model.
cc
The 11 Recommendations msgids (IntegrationsSettings RECOMMENDATIONS/toggle/
description, HomeView tab, ExternalRecoView row titles + empty state) had
zero catalog entries and fell back to English everywhere. Translate them in
es/de/fr/pt, plus the 2 new JACK-note strings. es is Mexican (register-
checked: no voseo/Castilian). ru/ja stay English-fallback pending a reviewed
pass.
cc
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
The Slint UI for the artist_vectors suggestions, 1:1 with the Svelte
PlaylistSuggestions component (backend T1-T7 already landed):
- T8: PlaylistSuggestionsState/Actions globals (distinct from the immersive
SuggestionsState) + a controller (crates/qbz/src/playlist_suggestions.rs)
that holds the pool/pagination/dedup and calls core.generate_playlist_suggestions
on a spawned task; wired into main.rs; per-row cover artwork target; suggestions
reset on playlist navigation.
- T9: the Suggested Songs section in PlaylistView.slint (docked after the
virtualized track list), manual activation via a header wand-sparkles button,
per-row Add/Play/info/dismiss + loading/empty states; a Close (x) button hides
the panel (activated is in-out).
- T10: per-playlist dismissed-track store (playlist_suggestions_dismiss.rs),
filtered out of the pool.
- i18n keys added to es/de/fr/pt.
Owner-built and verified working.
cc
Open the per-user ArtistVectorStore (<user>/cache/artist_vectors.db) and install
it on the core (core.set_artist_vectors) at the three session-activation sites
(login, restore, offline entry), next to the sub-plan-1 reco init; drop it on
logout (core.clear_artist_vectors). The store is core-owned (no bin-side static),
so the suggestions engine reads/writes it through QbzCore. Adds qbz-reco as a bin
dependency.
cc