Commit Graph
5421 Commits
Author SHA1 Message Date
vicrodh 6f7ff9956c feat(audio): surface "JACK is not bit-perfect" in Audio settings
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
2026-06-30 09:27:07 -06:00
vicrodh 67f5569af4 fix(ui): About modal density/height + splash disclaimer placement
- 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
2026-06-30 08:08:41 -06:00
vicrodh fb172335be fix(blacklist): align album-cover artwork jobs with the filtered model
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
2026-06-30 08:08:41 -06:00
vicrodh 66889af421 feat(blacklist): album-block context menu + manager Albums tab; albumview polish
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
2026-06-30 00:06:05 -06:00
vicrodh c85ad5f661 feat(blacklist): enforce album-level blocking across core + all consumers
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
2026-06-29 23:30:23 -06:00
vicrodh 3fbb166b3e feat(blacklist): add String-keyed album blacklist store in qbz-app
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
2026-06-29 23:03:47 -06:00
vicrodh be487dca4c feat(slint): work grouping, ru/ja locales, About + What's New + Discover reco modal
- 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
2026-06-29 22:55:17 -06:00
vicrodh e0e6ca81a9 fix(slint): queue sleep-timer flyout closed on any click
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
2026-06-29 16:58:16 -06:00
vicrodh 6f6f2d5aec feat(slint): whole-row selection in multi-select mode
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
2026-06-29 16:58:16 -06:00
vicrodh acfd3d928d feat(slint): keyboard multi-select on the built-out surfaces (Mix, Label, LocalLibrary Albums)
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
2026-06-29 14:53:31 -06:00
vicrodh 53e1e8cd1c feat(slint): keyboard-driven multi-select on existing surfaces
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
2026-06-29 13:58:18 -06:00
vicrodh 99da4c381b feat(slint): pre-launch dev panel, queue controls, appearance persistence, reco cache settings
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
2026-06-29 13:19:06 -06:00
vicrodh 158d6416ce fix(tray): debounce double-click to avoid wgpu TextureView panic
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
2026-06-29 13:18:51 -06:00
vicrodh b76e2b8137 feat(immersive): Liquid Spectrum visualizer POC (parked, disabled in menu)
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
2026-06-29 03:22:53 -06:00
vicrodh 226c2118ef feat(immersive): three new visualizer scenes (Wave Bed, Spectral Ribbon, Line Bed) + polish
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
2026-06-28 23:55:17 -06:00
vicrodh 2a8d01e686 feat(immersive): GPU shader scenes with album-art palette, audio uniforms, and song card
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
2026-06-28 15:36:07 -06:00
vicrodh 705b735393 feat(immersive): faster tunnel shader with winding path and vanishing-point speed-lines
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
2026-06-28 15:32:51 -06:00
vicrodh 98d606f913 fix(playlist): follow/unfollow button reflects the true follow state
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
2026-06-28 12:59:06 -06:00
vicrodh 1a4e3bd3d8 feat(sidebar): add search and import to the collapsed-sidebar playlists flyout
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
2026-06-28 12:58:17 -06:00
vicrodh be97d899d9 fix(sidebar): open the playlists options flyout to the right
The "..." options menu opened leftward over the playlist list; anchor it
to the right of the button (overlaying the content area) instead.

cc
2026-06-28 12:58:17 -06:00
vicrodh 9521c7e03e feat(playlist): hide Copy button for playlists already copied to library
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
2026-06-28 12:58:04 -06:00
vicrodh e0c921070c fix(npb-classic): "+" opens the grouped add flyout like the other layouts
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
2026-06-28 11:00:36 -06:00
vicrodh 59409822ab fix(npb-large): lift the cover 2px off the window bottom
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
2026-06-28 11:00:36 -06:00
vicrodh de32c1687f fix(playlist): owner-aware remove (delete vs unfollow) + refresh-in-place nav
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
2026-06-28 11:00:36 -06:00
vicrodh 6a17c48f59 feat(settings): appearance/integrations polish, functional window title, link modal
- 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
2026-06-27 23:15:45 -06:00
vicrodh 0f34a4280d fix(playlist): reserve back-to-top FAB clearance at the content end
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
2026-06-27 23:15:45 -06:00
vicrodh b5fd478b7b fix(foryou): radio excludes local/Plex, Plex/local artwork, radio nav buttons
- 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
2026-06-27 23:15:45 -06:00
vicrodh c3b921efff fix(reco): Weekly rows resolve fast, cache per-week, show per-row skeletons
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
2026-06-27 21:05:57 -06:00
vicrodh 1e78884e72 fix(reco): don't cache an empty-ListenBrainz result (Weeklys vanish bug)
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
2026-06-27 17:10:44 -06:00
vicrodh 42bb5910b5 revert(queue): restore the paginator (growing-list was wrong)
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
2026-06-27 16:53:23 -06:00
vicrodh 124d97e7d2 feat(reco): Recommended Albums — one per artist, >=20 albums
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
2026-06-27 16:53:23 -06:00
vicrodh 408223b4e1 fix(follow): reflect artist follow across every card surface
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
2026-06-27 16:53:23 -06:00
vicrodh 8107d657bb fix(playback): enrich radio metadata + push renderer queue on QConnect
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
2026-06-27 16:53:23 -06:00
vicrodh 1458214a91 fix(artist): add ArtistState.is-following for the follow heart
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
2026-06-27 15:44:30 -06:00
vicrodh c1b189363a i18n: translate the slint-polish batch UI strings
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
2026-06-27 15:34:22 -06:00
vicrodh d906e55647 feat(artist): make the follow heart + artist-card follow a working toggle
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
2026-06-27 15:21:42 -06:00
vicrodh 7b689398c7 fix(playlist): correct with_db result check in favorite toggle
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
2026-06-27 14:32:54 -06:00
vicrodh 2d7232d4e7 feat(discover): add "queue" + "create playlist" buttons to Weekly rows
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
2026-06-27 14:22:01 -06:00
vicrodh f51380f4f8 feat(queue): render up-next as one growing list (no pagination)
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
2026-06-27 14:16:39 -06:00
vicrodh 270850e352 feat(settings): MusicBrainz opt-out + gate MB-dependent sections
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
2026-06-27 14:11:25 -06:00
vicrodh bf4ffbd601 feat(playlist): Qobuz playlist Follow + Copy + internal favorite reflect
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
2026-06-27 13:58:58 -06:00
vicrodh bb385f8cdb feat(queue): wire infinite play (auto-advance + immersive toggle)
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
2026-06-27 13:50:04 -06:00
vicrodh b89009c254 feat(queue): wire "Save as playlist" to the add-to-playlist picker
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
2026-06-27 13:37:27 -06:00
vicrodh 6520c007ae fix(radio): repair /radio/* deserialization and wire every radio entry point
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
2026-06-27 13:34:31 -06:00
vicrodh 332fa430e5 feat(reco): Recommendations in all Discover flyouts + opt-out setting
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
2026-06-27 11:57:46 -06:00
vicrodh 05df99b713 fix(reco): full-albums-only + slop filter + taller Recommendation rows
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
2026-06-27 11:20:06 -06:00
vicrodh c6b4906a6b feat(reco): common/recent split + round-robin + 48h results cache + toast
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
2026-06-27 09:13:05 -06:00
vicrodh 0347e333a8 feat(reco): redesign Recommendations tab — artist/album-centric + LB playlists
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
2026-06-27 08:34:24 -06:00
vicrodh ec98245364 feat(reco): wire the external-recommendations 4th Discover tab (Slint)
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
2026-06-27 03:12:05 -06:00
vicrodh 408b3c8308 feat(reco): external-recommendations engine (Last.fm + ListenBrainz -> Qobuz)
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
2026-06-27 02:37:32 -06:00