Commit Graph
13 Commits
Author SHA1 Message Date
vicrodh 316a0c8f46 fix(qt): replace unavailable library favorites
cx
2026-09-05 17:31:17 -06:00
vicrodh 6d8c951a38 chore(deps): rusqlite 0.40 (u64 casts; a latent two-statement execute fixed)
rusqlite 0.40 removed ToSql/FromSql for u64 (SQLite integers are i64):
every u64 column now reads/writes through an explicit i64 cast. It also
rejects a multi-statement execute() instead of silently running only the
first statement — which is what catalog.rs's editions/logical_albums
prune had been doing all along; it is an execute_batch now, so the
logical_albums prune finally runs.

cc
2026-08-29 08:36:21 -06:00
vicrodh 371be04be8 feat(models): Track.streamable becomes Option<bool>, and absence stops meaning dead
`#[serde(default)]` on a bare bool means an absent key deserializes to FALSE,
so a catalog Track from any endpoint that omits `streamable` came back marked
unavailable. The models' own tests document this as a hazard rather than a bug
(the "streamable split": PurchaseTrack defaults true, catalog Track defaults
false, and the comment warns that a wrong default "inverts that whole screen's
behaviour"). The inferred OpenAPI disagrees with the code and documents
`default: true`.

That was survivable only because nothing read the field. It stops being
survivable the moment a frontend greys rows out and keeps them out of the
queue: one terse endpoint would blank an entire view, silently.

`None` now means the endpoint made no claim, and only `Some(false)` marks a
track pulled. `is_streamable()` is the ONE place absence is interpreted, and it
resolves to available: refusing to play a track because a response was terse is
strictly worse than playing one that turns out to be dead, which the reactive
skip path already handles. Album.streamable was already Option<bool> with a doc
comment naming the same causes, so this makes Track consistent rather than
novel.

Every consumer in the workspace moves to the helper: both matchers, the ISRC
validator, the daemon driver, the purchases service (which maps catalog Track
into PurchaseTrack and therefore feeds the very screen the split protects), the
radio pool, the mixtape resolver and the QConnect renderer. The replaced tests
pin both directions of the new contract.

Also here, because it lives in the same file as the matcher's streamable skip:
`rank_candidates`, an additive public entry point over the existing weighted
matcher (ISRC short-circuit, title/artist/album weights, duration bonus,
stop-word normalisation, quality tie-break). `match_tracks` is untouched.

cc
2026-08-17 14:40:57 -06:00
vicrodh 2ca7b672e0 fix(qt): album carousel cards — artist fallback and similar-album genre
Two card-data gaps on the album page's bottom rails:

- "Listening suggestions" (/album/suggest) rendered no artist line:
  the Qt mapping read only the flat `artist` field, while Slint's
  album_map::album_artist falls back to the artists[] array (main-artist
  role, else first entry) — the shape /album/suggest actually returns for
  some albums. The fallback is ported as card_artist.
- "Similar albums" (Last.fm row) showed no genre in the hover overlay:
  AlbumReco carried no genre at all. build_album_reco has the resolving
  Qobuz Album in hand, so the field now exists (serde-defaulted, so
  cached reco JSON keeps working) and is mapped through. Slint's
  external_reco::album_card can map it too after the next merge — its
  checkout is a different branch and is untouched here.

km
2026-08-10 20:23:45 -06:00
vicrodh 465f8627eb feat(discover): filter, cross-rail dedup, and overflow backfill for reco rails
The two Recommended-Artist rails showed artists the user already follows
or blacklisted, and the same artist could appear in both rails at once.

Builders now retain the full validated pool (rotated, not truncated at
DISPLAY_CAP); the pools ride the results blob as before, so old blobs
simply have no overflow until they expire. A new pure compose step in
qbz-external-reco (unit-tested: exclusion, within/cross-rail dedup with
common-wins ordering, visible/overflow split order) runs at the single
paint choke point in external_reco.rs — covering BOTH the fresh build
and the cached-blob paint. Exclusions fold followed (fav_cache) +
blacklisted + reco-dismissed ids, each source independently fail-open.
The past-cap survivors are retained as overflow for the Not-interested
live backfill (no extra network).

km
2026-07-22 22:08:38 -06:00
vicrodh 9b9bffbf64 feat(album): Last.fm similar-albums carousel under suggestions
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
2026-06-30 13:13:50 -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 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 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 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 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