Commit Graph
161 Commits
Author SHA1 Message Date
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 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 2e1d8e431d feat(reco): playlist Suggested Songs UI (T8-T10)
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
2026-06-27 01:57:30 -06:00
vicrodh c8ef2a1b1d refactor(slint): split qbz-slint into qbz-ui lib + qbz bin; reclaim qbz binary
Slint cannot split the .slint compilation across crates, but a Slint LIBRARY
crate exposes its generated Rust types to dependent crates (verified with a
minimal proof). So:

- NEW crate `qbz-ui` (lib): owns the whole .slint tree + build.rs + bundled
  translations; `slint::include_modules!()` + the generated types (AppWindow,
  MiniPlayerWindow, all globals/structs) are re-exported. Translation domain is
  now `qbz-ui` (the .po files renamed accordingly).
- `qbz-slint` (bin) -> renamed to `qbz` (package + `[[bin]] name = "qbz"`),
  reclaiming the binary name from the retired Tauri app. It depends on `qbz-ui`
  and does `pub use qbz_ui::*;` so existing `crate::AppWindow` / `crate::*State`
  references resolve unchanged.

This splits the single ~20 GB rustc into two crates (UI/codegen vs the 75k LOC
of logic) — the foundation for lower build peak, incremental caching, and the
upcoming per-feature crate extraction.

Also fixed for the move: qbz-i18n bundled-.po include paths, the font/WGSL
`include_bytes!`/`include_str!` paths (-> ../../qbz-ui/ui/), workspace members,
dev scripts (`-p qbz`), and the CI workflows. `cargo check -p qbz` is green.

cc
2026-06-25 17:51:09 -06:00