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
Add QbzCore::generate_playlist_suggestions + a per-user artist_vectors store
field (Arc<tokio::Mutex<Option<ArtistVectorStore>>>) with async
set_artist_vectors / clear_artist_vectors. It resolves each playlist artist NAME
to a confident MusicBrainz id (reusing musicbrainz_resolve_artist; the names come
from the playlist's own Qobuz tracks so they're already canonical, making the
Tauri command's extra Qobuz-name re-fetch unnecessary), then runs the
SuggestionsEngine over the core-owned clients + vector store.
Also aligns the qbz-reco builder/engine handle types to qbz-core's reality (the
plan's by-ref pattern doesn't fit): mb_client = Arc<MusicBrainzClient> (no lock,
Send+Sync); mb_cache = Arc<std::sync::Mutex<...>> (locked only in await-free
windows); Qobuz client = Arc<RwLock<Option<QobuzClient>>> (unwrapped per access).
The vector store stays tokio::Mutex (held across awaits by the engine). No
dependency cycle (qbz-reco does not depend on qbz-core). qbz-core checks clean;
23 qbz-reco tests green.
cc
T5: 1:1 port of the artist_vectors SuggestionsEngine into qbz-reco (~880 lines;
structure matches the Tauri source function-for-function). generate_suggestions:
ensure each seed vector (builder.ensure_vector), gate on a non-empty combined
playlist vector, rank candidates by DIRECT summed relationship weight via
store.get_all_related_artists (NOT cosine/find_nearest, D3), then pull + dedup +
exclude tracks from Qobuz with the validate_qobuz_artist + has_incompatible_genre
gates and optional reason strings. Keeps Tauri's Arc<Mutex>/RwLock ownership
(Send-safe). DTO reconciliation: crate::api -> qbz_qobuz::QobuzClient +
qbz_models::Track (4 Qobuz calls mapped to real signatures, field-by-field).
Adds rand (0.10, workspace) + qbz-models deps. 23 qbz-reco tests green.
cc
T4: 1:1 port of the artist_vectors vector builder into qbz-reco. Keeps Tauri's
Arc<Mutex>/RwLock ownership of the store + MusicBrainz client/cache + Qobuz
client (NOT the plan's by-ref pattern): MusicBrainzCache holds a !Sync rusqlite
Connection, so a borrowed &cache held across the MB-API await would make the
spawned suggestions future !Send. The interleaved short-lived locking (drop the
guard before each await) is the faithful + Send-safe shape. Swaps only
crate::api::QobuzClient -> qbz_qobuz::QobuzClient (get_similar_artists ->
SearchResultsPage<Artist>); the MusicBrainz types already live in
qbz_integrations so extract_relationships ports verbatim. Drops the dead
resolve_qobuz_to_mbid (D3). 21 qbz-reco tests green.
cc
T3: 1:1 port of the artist_vectors vector store into qbz-reco. open_at(base)
opens <base>/cache/artist_vectors.db (WAL) with the 3-table schema kept
byte-identical (CREATE IF NOT EXISTS) for cross-frontend reuse. Ports the
in-memory artist index, get_or_create_idx, set_vector (delete-then-insert per
(artist,source) + metadata nnz), get_vector (SUM GROUP BY), has_fresh_vector,
get_artist_name, and the production ranking get_related_artists /
get_all_related_artists (SUM ORDER BY total_weight DESC). Drops the dead
find_nearest cosine path (D3) and the Tauri State/tokio wrapper (lifecycle in
the frontend/core per ADR-006). 4 store tests, 20 qbz-reco tests green.
cc
T2: 1:1 port of the artist_vectors RelationshipWeights (12 fields, Default, the
band_focused/similarity_focused presets, and the weight_for_mb_relation /
weight_for_source maps) with its tests. The presets are unused in production but
ported verbatim for parity (pub lib API, no dead-code warnings).
cc
New frontend-agnostic crate qbz-reco (ADR-006) for the playlist Suggested Songs
engine (sub-plan 2). T1: 1:1 port of the Tauri artist_vectors SparseVector
(sorted parallel index/value arrays, set/get, dot merge-join, magnitude/
normalize, serde) with its full test suite (13/13 green). Drops the dead
top_k_similar helper (its only caller find_nearest is dropped per epic D3);
keeps cosine_similarity for parity + tests but not for ranking.
cc
Add RecoStore::get_known_artist_ids (artists played >threshold OR favorited;
unit-tested) and union it into the network-discovery 'skip artists I already
know' filter. play_history stays the source of the (id, name) known set:
reco_events has no artist names and the schema is frozen to match Tauri, so
full consolidation (plan D2) would regress the name-based MB candidate filter.
This augments the id set with reco's richer plays+favorites signal instead.
cc
Reorder the resolved favorite albums so the reco-scored favorites lead (taste
order from the trained favorite_album scores), keeping unscored albums in their
original relative order. When reco is cold/disabled the row keeps the original
Qobuz order, so a fresh store never regresses. The reorder flows into the
Favorites, Rediscover, Radio and more-from-library seed (all share fav_albums).
cc
Wire the reco store into the For You favorite-albums branch:
- Genre backfill: when favorite albums resolve (full Album with genre), write
each (genre_id, genre_name) onto its reco events + album-meta via
spawn_blocking. Plays carry no genre, so this makes the engine's get_top_genres
non-empty (Tauri never backfilled, so its top-genres was dead).
- Rediscover: build_rediscover prefers the reco 'forgotten favorites' set
(favorited, not played in 30d, from the Tauri-shared events.db) when warm,
falling back to the local recents heuristic when cold so the row never empties
(non-regressing for fresh stores, better for Tauri-migrants).
cc
Two additive RecoStore queries with unit tests (all 8 reco_store tests green):
- get_forgotten_favorite_album_ids(limit, recency_days): favorite albums not
played within the window. Ported from Tauri db.rs:391-426 but adapted to the
integer created_at schema -- Tauri's datetime('now',..) string compare never
matched the integer column, so the recency filter is a numeric cutoff.
- update_genre_for_album(album_id, genre_id): backfills genre_id onto an
album's still-NULL events. Plays log no genre, so the frontend writes it back
on album resolution; this is what makes get_top_genres non-empty (Tauri
defined the backfill fns but never called them, so its top-genres was empty).
cc
mix_listened_seed_ids now prefers the reco store's scored continue-listening +
favorite track seeds (generous per-bucket limits, deduped, capped at 120) and
falls back to the local recents + Qobuz-favorites derivation when reco is
cold/disabled, so mixes never go empty. Call site unchanged. Drops the now
redundant dead_code allow on reco::home_seeds (W6 is its first consumer).
cc
Replace the 10 TODO(reco-v1) markers with PlaylistAdd logging, one event per
requested Qobuz track id:
- Qobuz->Qobuz (no-dup + dup-fallback), create-and-add online, dup-confirm
add-all and add-new-only, Qobuz->local, create-and-add local (Qobuz branch):
log the FULL requested ids (cloned before any dedup/insert consumes them),
with the Qobuz playlist id when the target is a Qobuz playlist.
- Local-ref adds (2 seams) and the playlist importer are NOT logged: local
refs are not resolvable Qobuz ids (same source gate as plays), and the
importer is a bulk external import Tauri never logged.
cc
Log a reco favorite event at each favorite-add seam, ADD-only and on network
success so un-favorites, rollbacks and bulk syncs never enter the signal:
- track-row heart (main.rs) + now-playing heart (queue.rs) -> track favorite
- album grid-add arm + album-header toggle -> album favorite
- artist follow -> artist favorite (Qobuz id parsed to u64)
Each logs via spawn_blocking off the async path. The album-toggle and artist
seams log from the async block, not the upgrade_in_event_loop UI-thread
closure (where spawn_blocking would panic).
cc
Fire RecoStore::train() fire-and-forget (spawn_blocking) right after the store
is opened on login and session-restore, mirroring Tauri's non-awaited
trainScores. Off-thread, default decay/weight params, no-op when reco is
disabled. Offline entry stays init-only.
cc
Wire the ported, unit-tested RecoStore engine into the session lifecycle and
log plays at the recents chokepoint.
- New crates/qbz/src/reco.rs: per-user store behind a process-global
Mutex<Option<RecoStore>> (mirrors fav_cache); helpers no-op when no session
is open. init_for_user/teardown, home_seeds reader, is_qobuz_source gate,
log_play_gated.
- Lifecycle: init on login/restore/offline-entry next to fav_cache; teardown
on logout.
- playback::record_recent logs each play via spawn_blocking (blocking SQLite
off the async path), gated Qobuz-catalog only (skip local/plex/ephemeral so
unresolvable ids never poison the home seeds; qobuz_download counts).
cc
Feature branches (slint-mvp) no longer auto-run the build on every push — that
burned minutes, and the qbz_ui compile is the expensive one. build-slint.yml now
auto-fires only on push to main; workflow_dispatch keeps it manually triggerable
on any branch.
cc
Adds cargo-deb / cargo-generate-rpm config so the release pipeline can fan out
.deb/.rpm from the prebuilt (stripped) binary — no Tauri bundler needed.
- [package.metadata.deb] + [package.metadata.generate-rpm] in crates/qbz.
- packaging/qbz.desktop (FHS) + packaging/qbz.png (icon) -> /usr/share/{applications,icons}.
- Runtime deps: $auto (ldd) + the dlopen'd GPU/Wayland/D-Bus stack listed by hand
(ldd can't see it). Validated in a 16 GB Ubuntu 24.04 VM: 37 MB .deb, 44 MB .rpm.
cc
Not every album ships a booklet and the in-app PDF reader was clumsy. The album
booklet button now DOWNLOADS the goody PDF (native save dialog, app HTTP client)
instead of rendering it in-app, and the MuPDF rasterizer is dropped entirely:
- crates/qbz/src/booklet.rs: reduced to set/clear URL + download_booklet (fetch
+ save dialog). Removed the reader (open/render/paging/zoom/rotate/close).
- main.rs: the ("album","booklet") action now calls download_booklet; removed the
BookletActions reader wiring block.
- Removed the qbz-booklet crate (MuPDF) from the workspace + qbz deps; Cargo.lock
drops mupdf/mupdf-sys. ~26 MB static MuPDF (~6 MB linked) + its heavy C build
are gone, lightening the build (more headroom under the 16 GB rustc wall).
AlbumBookletModal.slint + the BookletState/BookletActions globals are now unused
(left in place; remove in a UI cleanup pass that recompiles qbz-ui).
cc
The dev box is Gentoo/OpenRC with no systemd, hence no snapd, so it cannot
build or upload snaps. This pipeline keeps everything on GitHub-hosted Ubuntu
runners (which have snapd) instead:
- packaging/snap-slint/snapcraft.yaml: a `plugin: dump` recipe that packages a
PRE-BUILT qbz binary (no compilation), with the Slint runtime stack
(Vulkan/GL via the gnome extension, winit Wayland/X11, alsa/pipewire, mpris).
Kept separate from the Tauri snapcraft.yaml so neither clobbers the other.
- .github/workflows/release-snap.yml: job 1 compiles the binary on a 16 GB
runner using the same MIN-tier + 18 GB swap recipe as build-slint.yml; job 2
downloads it and runs snapcraft `dump` + snapcore/action-publish. The heavy
compile is on a free public runner; the snap packaging is on a runner that
has snapd — neither touches the OpenRC box.
Requires the SNAPCRAFT_STORE_CREDENTIALS secret (generate once via
`snapcraft export-login` on any snapd-capable host). The runtime confinement
(stage-packages/plugs for the non-GTK GPU stack) is a first cut and will need a
real `snapcraft build` + `snap run` on the runner to finalize; it cannot be
tested locally without snapd.
cc
The qbz_ui rustc (generated Slint UI, one ~1.6M-line module) is the build
wall: measured peak demand ~30 GB (RSS+Swap), and it is frontend-bound.
Codegen knobs barely move it (cu=256/opt=2 + -Z threads=1/jobs=1 shave only
~3 GB vs cargo defaults), so the build is made to FIT via RAM+swap, not tuned
down. Extracting feature crates out of the bin does not lower it either: the
bin peaks at only ~4.5 GB; qbz_ui is strictly upstream of it.
- Swap 8 -> 18 GB: 16 GB RAM + ~22 GB swap (~38 GB) comfortably exceeds the
~30 GB demand. 8 GB was an OOM risk (16+4+8 ~= 28 GB < 30 GB).
- Swatinem/rust-cache@v2 (crates -> target, shared-key, save-if slint-mvp):
a controller-only push restores the prebuilt qbz_ui rlib from cache and
rebuilds only the cheap qbz bin. This, not the peak, is the minute saver.
- Pin a dated nightly (was floating @nightly): rustc -V is part of the cache
key, so a floating nightly re-keyed the cache and invalidated every Cargo
fingerprint daily (the from-scratch-every-run waste).
- concurrency cancel-in-progress: a new push to slint-mvp cancels the older
in-flight build instead of stacking ~20-min runs.
cc
New shared crate qbz-slint-common (will also host the artwork primitives via a
dependency-inversion pass). toast.rs is a clean leaf over qbz-ui: only Slint
types (AppWindow, ToastKind, ToastState) + slint core (Timer/Weak). Moved
verbatim; the only edit is the import (`crate::` -> `qbz_ui::`). Bin re-exports
via `pub use qbz_slint_common::toast;` so all 154 call sites across 21 files are
unchanged. slint dep is `default-features = false` (adversarial review: a
default-features slint here would turn on the default renderer for the whole
build via feature unification). `cargo check -p qbz` green.
cc
Both are pure leaves (no bin-module refs, no Slint coupling). Moved verbatim
into qbz-text-utils. Caught by adversarial review: dates.rs needs chrono's
`unstable-locales` feature (Locale + format_localized) and an inline
`qbz_i18n::current_language()` ref (no `use` line — the qbz_models::Track
trap). Bin re-exports via `pub use qbz_text_utils::{dates, strip_html};` so the
14 call sites are unchanged. `cargo check -p qbz` green.
cc
First per-feature crate split off the qbz binary. `dac_wizard.rs` (794 LOC)
was the cleanest candidate: zero cross-feature module deps — it only used
qbz-ui generated types (AppWindow, DacWizardState, …), qbz-audio, qbz-models
and qbz-i18n. Moved verbatim into `qbz-dac-wizard/src/lib.rs` (import
`crate::` -> `qbz_ui::`); the bin now `use qbz_dac_wizard as dac_wizard;` so
its ~20 call sites in main.rs are unchanged. `cargo check -p qbz` green.
cc
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
The GitHub-hosted private runner is 2 vCPU/8 GB and cannot build qbz-slint
(died at 1h42m). The dev box CAN, so a self-hosted runner (qbz-box) gives a
reliable CI artifact. Manual trigger; uncapped threads=2 (box-proven);
persistent work dir = incremental after first build.
cc
mupdf-sys (via qbz-booklet) links the system third-party libs; the build
got 21 min in and failed on a missing harfbuzz. Add the full mupdf system-lib
set so it links cleanly.
cc
The runner already ships an active /swapfile, so fallocate on it fails with
"Text file busy". Add the extra swap on a fresh path (/swap_extra) and guard
the whole swap block so a hiccup never fails the job — memory was never the
blocker (the prior run died on disk, not OOM).
cc
ubuntu-24.04 has no separate large /mnt disk — it's all on the 72 GB root
(~16 GB free), so the 12 GB swapfile + cargo target overflowed it (protobuf
failed with ENOSPC, not OOM — the build was memory-fine). Free ~25-30 GB of
unused preinstalled toolchains, drop swap to 8 GB, and keep the target on the
default path.
cc
Build-only workflow on ubuntu-24.04 with a 12 GB swapfile and the
lowest-memory rustc settings (serial frontend, codegen-units 256,
opt-level 2, one rustc at a time) so a stock 16 GB runner can compile the
Slint app. Triggers on push to slint-mvp and via workflow_dispatch.
cc
Adds the full keybindings system (26 actions, 5 categories, customizable
and persisted) plus the Ctrl+L "Open Qobuz Link" cross-platform resolver
to the Slint frontend, matching the Tauri implementation 1:1.
- New shared crate qbz-music-link: de-Tauri'd cross-platform link
resolver (Qobuz/Spotify/Apple/Tidal/Deezer/song.link via Odesli) behind
a QobuzSearchBridge trait. src-tauri left untouched.
- src/keybindings.rs: action model, winit shortcut grammar, conflict
detection, ui_prefs persistence, main-window + miniplayer dispatch.
- src/link_resolver.rs: controller + bridge over QbzCore.
- New globals + 3 modals (read-only cheatsheet, customize editor with key
capture, link resolver), hamburger menu entries, and a UiFocusState
text-input guard wired across the app's TextInputs.
cc
The fixed threads=16/opt=3 release build SIGTERM'd at codegen whenever the
desktop held ~8-11 GB (30 GB box, no hibernation, hard-freezes on OOM). Now
scales rustc frontend threads + codegen-units + opt-level to MemAvailable
(FAST >=26G / SAFE 14-26G / MIN <14G), runs under cargo-capped with the cap
matched to free RAM, and adds FAST/THREADS/CODEGEN_UNITS/OPT/CAPPED/NORUN env
overrides. Verified: full release build completed at SAFE tier (threads=2
cgu=256 opt=3, capped ~19.7G) in 55m without freezing.
cc
The public getter was redundant: format-scoped completion gating reads the
download format_id internally via track_status_scoped (s.album_format_id), so the
accessor was never wired and only produced a dead_code warning. No behavior
change. cargo check -p qbz-slint exit-0 in 2m14s; the port code is now
warning-free (the remaining following_state/cached warnings are pre-existing,
unrelated to purchases).
cc
Closes the final-audit gaps for full 1:1 + standards compliance:
- Persist the 4 remaining purchase UI prefs (hide-unavailable, hide-downloaded,
quality-filter, region-notice-seen) per-machine in ui_prefs.json + startup seed,
so filter selections survive restart and the region notice does not reappear
(matches Tauri's persisted prefs).
- Translate the 4 controller toast/error strings into es/de/fr/pt (were
English-only; honors the 5-locale rule). Mexican/neutral Spanish.
- Explicit z:3000/3001 on the format-picker backdrop/popup + filter panel
(ADR-009 >= 3000; previously relied on declaration order).
cargo check -p qbz-slint exit-0.
cc
Slice 12/13. Two persisted UI prefs (show_purchases, nav_tb_purchases, both
default false) in UiPrefs; Show-Purchases toggle drives the show-purchases flag
the S11 nav gates on; 'Purchases in title bar' toggle (nav_tb_purchases, disabled
without a custom titlebar). Implements the source-of-truth section 5.8 placement
truth table: sidebar entry shown iff S && !(T && TB), header entry iff S && T && TB
(closing the S11 per-feature-gating gap) so flipping the toggle moves the Purchases
entry between sidebar and header 1:1 with Tauri. on_appearance_bool persistence +
startup reseed. cargo check -p qbz-slint reached exit-0. Per-machine prefs
(consistent with other Slint appearance prefs).
cc
Slice 11/13. NavEntry::PurchaseDetail(String) + full routing (scope_for/
apply_entry, history recording + update_nav_flags on purchase-album clicks and
the Purchases route), session-restore-safe purchases (id-gated purchase-album),
conditional-mount guard + A.3 reactive reload (back/forward between two purchase
albums re-fetches). Sidebar ShoppingBag row + HeaderBar tabs (full+compact),
gated on show-purchases && !offline, active for both purchases + purchase-album.
nav.rs round-trip history test. Type-check clean (exit-0 not reachable on this
box; SIGTERM at metadata = memory limit).
cc
Restores the purchase-album detail mapper that the Slice-9 detail load path
(qbz-slint purchases.rs:2270) already references but which was left uncommitted
in the working tree (staged-list miss when committing S9). build_purchase_album
maps a fetched Album + purchase listing + downloaded ids + format map into the
detail payload (per-track + album downloaded flags, synthesized format options,
page counts). 5 unit tests; 30 offline-cache purchases tests green.
cc
Slice 10/13. A.2: per-track row play in both purchases views routes to
playback::play_track_now (single track, NO replacePlaybackQueue/setPlaybackContext)
instead of media_action play_track_in_context. A.1: Play-album confirmed on the
regular album fetch+queue path. New mirror_now_playing_to_purchases feeds both
purchases globals' active-track-id + playback-active from the now-playing refresh
(meta apply + poll tick). Folded Slice-9 review fixes: detail + list track titles
trimmed (formatTrackTitle parity), dead PurchaseDetailState.artwork-url removed.
Fixed E0308 (track_id u64). Front-end type-check clean (zero diagnostics).
cc
Slice 7/13. qbz-slint/src/purchases.rs: ports purchaseDownloadStore as a Send
OnceLock<Mutex<>> controller singleton (AlbumDownloadState keyed by albumId,
flattened allTrackStatuses, per-album abortFlags). Actions: start/execute album
download (sequential loop, abort-check, destination rewrite after first success),
start_track_download (merges into existing state, A.7), cancel, Add-to-Library,
folder picker, skip-if-remote.
1:1 fidelity fixes surfaced during review (verified vs Svelte source):
- Album loop marks complete THEN best-effort registry write (swallow failure)
like executeAlbumDownload .catch(()=>{}); only the single-track path propagates
a registry error to failed (B.1). Adds download_purchase_track_file_only to
qbz-offline-cache (download-only) beside the bundled download_purchase_track.
- apply_cancellation cancels only tracks with NO status entry (verbatim Svelte).
cargo check + 20 qbz-slint / 25 offline-cache purchases tests green.
cc
Slice 6/13. qbz-slint/src/purchases.rs: get_downloaded_purchase_formats
(HashMap track_id -> Vec<format_id>, the format_map) and quality_dir(label)
(/-> - global replace + trim, section 7.5), via library_db::with_db. Unit test
for quality_dir. Dead-code until slices 7-9 wire the call sites.
cc
Slice 3/13. New qbz-offline-cache/src/purchases_service.rs (pagination glue,
get_purchase_total two-call per-type-totals primitive, byte-for-byte
filter_purchase_response). New qbz-slint/src/purchases.rs controller load shells
(get_downloaded_track_ids -> HashSet, load_purchases_metadata with TWO separate
ids-totals calls, load_purchases_by_tab with total backfill + error mapping).
Replicates Tauri's 'Failed to fetch ... purchases' wrapper so network errors map
to purchases.loadFailed 1:1 (TDD-caught). 10 unit tests green. Lazy/cache/force
gating deferred to UI slices 8/9 by design.
cc
Slice 2/13. New impl QobuzClient block in qbz-qobuz/src/purchases.rs ported
1:1 from src-tauri api/client.rs: typed/ids purchase listing (page_limit 500,
two-call per-type totals quirk preserved), get_track_file_url_by_format
(RPC-signed, intent=stream, empty-url -> TrackUnavailable), and download_audio
(http1_only, NO total timeout). Adds 2 endpoint consts; bumps
http()/secret()/authenticated_headers() to pub(crate) for the sibling module.
Offline-gated via self.http()?. Drops .use_native_tls() (crates/ workspace is
rustls-only; matches cmaf.rs build_cdn_client) to keep the standalone build green.
cc
Slice 1/13 of the Purchases Tauri->Slint port. Adds PurchaseResponse,
PurchaseIdsResponse, PurchaseAlbum, PurchaseTrack (no version field) and
PurchaseFormatOption to qbz-models, plus the 5 lenient deserializers ported
verbatim from src-tauri api/models.rs. Derives Default on SearchResultsPage
for the empty-page fallback. 4 round-trip tests, cargo test green.
cc
select_bundled_translation must run after AppWindow::new() (the component's
global translation context does not exist before then), so startup now paints
in the persisted language instead of falling back to English. Adds
reseed_i18n_labels() which seeds the 14 AppearanceState option arrays from
qbz_i18n::t() — these were @tr() property defaults, which are not reactive to
translation changes — called at startup and on live language switch so the
settings <select> options translate and update without restart.
cc
Fills all previously-untranslated msgstr (UI strings absent from the Tauri
JSON seed) via a translation pass, then hand-fills the remainder. Neutral
Latin-American Spanish; standard professional register for de/fr/pt; brand/
technical tokens (FLAC, kHz, Plex, etc.) kept literal; {} placeholders and
gettext plural forms preserved. All four catalogs pass msgfmt -c.
cc
Adds qbz_i18n::mark() (identity, extraction-only) and applies it at the
definition sites of literals later translated through t(variable):
release-type classifier, discover section labels, artist release-section
titles + month names, label jump tabs, and audio settings dropdown tables.
Fixes the discover_prefs double-t() (label_for now returns the marked
literal, translated once at the consumer).
cc
Wrap user-facing Rust-built strings in qbz_i18n helpers across the
Artist/Album/Label surfaces: artist release-section titles + jump-tab
labels (routing kept on the English keys, only displayed labels
translated), MusicBrainz month-name/date formatting, album track-count
subtitle + "From the same artist"/"Listening suggestions" carousels,
label carousels + jump tabs + group-by "Unknown", and the shared
album_map release-type labels (Album/EP/Single/Live/Compilation).
cc
Wrap user-facing Rust-built strings in qbz_i18n helpers across the
Discover/Home/Search surfaces: discover section titles, configurator
labels (discover_prefs::label_for), For You carousel titles, mix
subtitles + duration, suggestions track-count + Song Radio, search
section headers + count subtitles, and the Lossless quality tooltip.
Reuses shared msgids ({} track/{} tracks, {} album/{} albums, section
names) so the catalog stays deduped.
cc
Wrap user-facing eyebrow labels (MIXTAPE/COLLECTION/ARTIST), album/item
counts, add/edit/builder/cover toasts, dialog titles + image filters,
play/queue toasts, and the default discography name in the My QBZ
controllers with qbz_i18n t/t_args/tf helpers. Keep the My QBZ brand
itself untranslated via positional interpolation.
cc
Wrap user-facing toasts, dialog titles/descriptions, durations,
track/folder counts, scan/cleanup status, and grouping labels in the
Local Library controllers (local_library, local_library_settings,
local_playlist, tag_editor) with qbz_i18n t/t_args/tf helpers.
cc