Commit Graph
5362 Commits
Author SHA1 Message Date
vicrodh c6fb6953bf feat(reco): order For You favorites by trained taste score
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
2026-06-26 20:00:39 -06:00
vicrodh e97d43d61a feat(reco): genre backfill + reco-backed Rediscover row
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
2026-06-26 19:57:07 -06:00
vicrodh bd887d755c feat(reco): add forgotten-favorites + genre-backfill engine queries
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
2026-06-26 19:51:31 -06:00
vicrodh 20a5858845 feat(reco): seed DailyQ/WeeklyQ mixes from reco home-seeds
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
2026-06-26 19:44:41 -06:00
vicrodh 4379400bc8 feat(reco): log playlist-add events at the 10 commit seams
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
2026-06-26 19:42:15 -06:00
vicrodh da2f0a88dc feat(reco): log favorite-ADD events (track, album, artist)
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
2026-06-26 19:33:04 -06:00
vicrodh 14b8a4006a feat(reco): train recommendation scores after session activation
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
2026-06-26 19:28:07 -06:00
vicrodh 4828e54cc3 feat(reco): per-user RecoStore wrapper + Qobuz play-event logging
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
2026-06-26 19:26:50 -06:00
vicrodh 94aa3782d2 ci: auto-trigger build-slint only on main; manual elsewhere
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
2026-06-26 17:23:11 -06:00
vicrodh 9c37e456f5 build(packaging): deb + rpm packaging metadata for the Slint qbz binary
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
2026-06-26 17:22:27 -06:00
vicrodh 998ab9d5c0 refactor(slint): booklet button downloads the PDF instead of the in-app MuPDF reader
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
2026-06-26 17:20:15 -06:00
vicrodh 77a476c8e7 ci(snap): all-hosted Snap pipeline for the Slint build (dump prebuilt binary)
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
2026-06-26 08:32:26 -06:00
vicrodh 00ef194a93 ci(build-slint): cacheable build + swap sized for the ~30GB qbz_ui peak
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
2026-06-26 06:38:24 -06:00
vicrodh d1437c70eb refactor(slint): extract toast into qbz-slint-common crate
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
2026-06-25 18:15:03 -06:00
vicrodh 6f9ad4668e refactor(slint): extract strip_html + dates into qbz-text-utils crate
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
2026-06-25 18:13:55 -06:00
vicrodh 7678bceb3e refactor(slint): extract DAC wizard into qbz-dac-wizard crate
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
2026-06-25 17:54:10 -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
vicrodh b9eb300516 ci: self-hosted runner workflow for qbz-slint (real artifact path)
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
2026-06-25 13:32:06 -06:00
vicrodh a4371f294b ci: add mupdf-sys system libs (harfbuzz, jbig2dec, openjp2, jpeg, zlib, gumbo)
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
2026-06-25 10:31:25 -06:00
vicrodh 2f58778abd ci: fix swap setup in slint build
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
2026-06-25 10:08:17 -06:00
vicrodh 0b466474ef ci: fix runner disk exhaustion in slint build
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
2026-06-25 10:04:54 -06:00
vicrodh 5806b0c057 ci: build qbz-slint on GitHub Actions (linux)
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
2026-06-25 09:43:50 -06:00
vicrodh 337062f316 feat(slint): port keyboard shortcuts + Open Qobuz Link from Tauri
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
2026-06-25 09:43:50 -06:00
vicrodh 14e800ea1f chore(scripts): make slint-run.sh memory-aware to skip the OOM wall
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
2026-06-24 21:01:32 -06:00
vicrodh 60dead32e4 fix(purchases): remove unused get_album_download_format_id accessor
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
2026-06-24 19:42:07 -06:00
vicrodh ce963c8b9f fix(purchases): persist filter prefs + translate 4 toasts + explicit popup z-index
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
2026-06-24 18:38:11 -06:00
vicrodh 9fbfcb5582 feat(purchases): i18n catalogs + gold purchase badge (Slint)
Slice 13/13 (final). Reconciles the download-progress interpolation in
PurchaseDetailView to proper Slint @tr placeholders (Downloading {}/{},
Download cancelled ({}/{}); tracksCount already placeholder). Fills the 4 non-en
.po catalogs (es/de/fr/pt) with 29 new purchases msgids each, in lock-step, no
duplicates, FR plural untouched, en catalog not modified. Adds the gold
qobuz_purchase source badge (24x24 chip on AlbumCard + inline gold-tint glyphs on
AlbumListRow/TrackRow/LocalAlbumView) gold #eab308. group.*/view.* not renamed.
Build: .po re-bundle + Slint macro expansion compile clean.

cc
2026-06-24 18:22:23 -06:00
vicrodh 45be8763d7 feat(purchases): add Settings toggles + nav placement truth table (default OFF)
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
2026-06-24 17:41:43 -06:00
vicrodh 965c898b10 feat(purchases): wire navigation + view persistence (Slint)
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
2026-06-24 17:12:58 -06:00
vicrodh eab178c1e3 feat(purchases): add build_purchase_album detail mapper (service)
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
2026-06-24 17:12:58 -06:00
vicrodh 18827eaab4 feat(purchases): wire play routing + now-playing active-track (Slint)
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
2026-06-24 16:14:41 -06:00
vicrodh be87fe3377 feat(purchases): add PurchaseDetailView UI (detail + download screen, Slint)
Slice 9/13. New ui/purchases/PurchaseDetailView.slint cloning AlbumPageView
header (224x224, unavailable TriangleAlert overlay, LONG-month purchasedOn,
quality/stats), CircleAction Play-album + Download-All, format dropdown,
count-based progress (NOT format-scoped, A.4), Add-to-Library block, disc-grouped
track table with 4-state hover download, getTrackStatus format-scoped suppression,
bare track-row quality (A.6). Wires PurchaseDetailState/PurchaseActions globals
(state.slint + app.slint bridge), AppShell purchase-detail mount, controller
load_purchase_album + apply_detail, detail-cover artwork target. 28 tests green.

Note: per-track/Play-album routing (A.1/A.2) + now-playing active-track wiring
land in Slice 10; nav history + reactive-reload in Slice 11.

cc
2026-06-24 14:02:15 -06:00
vicrodh 06dd52e5d4 feat(purchases): add PurchasesView list UI + nav mount (Slint)
Slice 8/13. New ui/purchases/PurchasesView.slint (list surface: Albums/Tracks
tabs+counts, debounced search, group/sort/filter, grid/list toggle, format-picker
popup, quality radios, unavailable overlay, bare track-row quality A.6) reusing
SegmentedTabBar/ExpandableSearch/QbzSelect/AlbumCollectionView/TrackRow. Wires
PurchasesState/PurchasesActions globals (state.slint + app.slint export bridge),
AppShell mount on ContentView.purchases, NavEntry::Purchases, artwork target,
controller apply/derive/actions in purchases.rs, shopping-bag/download icons.
28 unit tests green.

1:1 fix: download-track fetch error shows downloadFailed toast (not noFormats);
get_album_formats returns Result to distinguish fetch failure from empty list.

cc
2026-06-24 09:53:39 -06:00
vicrodh 44aa4e1462 feat(purchases): add download state machine + actions (controller)
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
2026-06-24 06:23:04 -06:00
vicrodh 76d0fd6b57 feat(purchases): wire registry reads + qualityDir in controller
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
2026-06-24 05:01:41 -06:00
vicrodh 9e676441e4 feat(purchases): add single-track download primitive
Slice 5/13. qbz-offline-cache/src/purchases_service.rs: download_purchase_track
(get_track -> getFileUrl(format) -> download_audio -> .part write -> rename ->
registry), target_path (sanitized {dest}/{artist}/{album [quality]}/{NN-title.ext}),
purchase_extension (keys off RESPONSE format/mime). Blind-test quirks 1:1:
records REQUESTED format_id while ext derives from RESPONSE (B.2), no collision
preflight / silent overwrite (B.3), restrictions ignored (B.5), file->rename->
registry ordering with orphaned-file-on-registry-failure (B.1). Tests green.

cc
2026-06-24 04:37:05 -06:00
vicrodh b87378349e feat(purchases): add format synthesis + download-flag annotation
Slice 4/13. qbz-offline-cache/src/purchases_service.rs: synth_formats(&Album)
ports v2_purchases_get_formats (ids 27/7/6/5, exact labels, highest-first order,
hires + sr>96 conditions, default index 0); apply_download_flags ports
v2_apply_purchase_download_flags (per-track downloaded + downloaded_format_ids;
per-album all-nested-ids-in-dlIds with empty->false page-mode gotcha). Frontend
overrides backend downloaded. Unit tests green.

cc
2026-06-24 04:25:03 -06:00
vicrodh bc44fe5e0c feat(purchases): add pagination + per-type totals service + controller load shells
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
2026-06-24 04:16:14 -06:00
vicrodh 26b6480ff1 feat(purchases): add purchase HTTP + getFileUrl + CDN fetch
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
2026-06-24 02:08:08 -06:00
vicrodh aceaf9b039 feat(purchases): add purchase wire models + lenient deserializers
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
2026-06-24 01:48:34 -06:00
vicrodh 502b0dae2b i18n: apply bundled translation after window init + reseed option arrays live
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
2026-06-24 00:04:32 -06:00
vicrodh 24da9f1b74 i18n: translate full catalog to es/de/fr/pt (100% coverage)
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
2026-06-23 21:45:05 -06:00
vicrodh 25f9a15f90 i18n: register dynamic msgid literals via mark() and fix double-translation
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
2026-06-23 21:31:42 -06:00
vicrodh 294f01d8bb i18n: re-extract fresh Slint pot (capture option arrays + Task-10 strings)
cc
2026-06-23 21:25:24 -06:00
vicrodh 630663fc4a i18n: extract Rust msgids and re-seed catalogs
cc
2026-06-23 15:15:04 -06:00
vicrodh e167936cc1 i18n(rust): misc strings
cc
2026-06-23 15:11:43 -06:00
vicrodh ed6bda2108 i18n(rust): integrations + dac wizard strings
cc
2026-06-23 15:11:38 -06:00
vicrodh 150ca7f2fc i18n(rust): playback/offline strings
cc
2026-06-23 14:55:18 -06:00
vicrodh f26e1a6626 i18n(rust): artist/album/label strings
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
2026-06-23 14:47:57 -06:00
vicrodh b2facfbd24 i18n(rust): discover/home/search strings
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
2026-06-23 14:47:50 -06:00