Commit Graph
16 Commits
Author SHA1 Message Date
vicrodh dfc74fb6d4 Merge branch 'pr/606' into integration/pr-review-smoke-2026-07-13 2026-07-13 14:27:23 -06:00
Afonso Jorge Ramos 61df66c529 fix(offline): purge only allowlisted cache layout directories
Stop recursive-deleting arbitrary siblings of the offline root so a
mis-set cache path cannot wipe unrelated folders on a shared mount.
2026-07-13 13:49:55 +02:00
Afonso Jorge Ramos 7e824bd65a fix(offline): place download size validator at module scope 2026-07-12 23:57:18 +02:00
Afonso Jorge Ramos f507b5c214 fix(offline): reject truncated downloads when Content-Length is known
Fail closed on short bodies and empty payloads so partial files never
become complete cache entries.
2026-07-12 23:56:59 +02: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 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 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 63e0e386ba feat(slint): plex rows attach on upload, manager sort interleaving, offline cover chain
Upload to Qobuz now attaches Plex sidecar rows via the existing
add_plex_track_to_playlist helper (positions after the local block), and
the local entity is deleted only when the sidecar attach succeeds - a DB
failure keeps the playlist with an upload-incomplete toast instead of
silently dropping it.

Playlist manager: local playlists interleave into the active sort with the
same comparators as the Qobuz set (name/tracks real interleave; playcount
sorts locals as zero, custom as after-the-positioned-set, recent after the
API order - documented). u64-keyed mutators verified safe with local: ids.

Offline covers: cached_tracks.artwork_path turned out to have zero writers
in history, so CachedTrackInfo gained artwork_path + file_path and a
frontend-agnostic resolve_cover_path chain (index path, CMAF cover.jpg,
v1 sibling cover.jpg). The offline manager album rows and the local-
playlist cached rows now resolve covers through it.

cc
2026-06-10 01:51:51 -06:00
vicrodh 4041499114 fix(offline-cache): run the secret-vault open off the async pool (no zbus panic)
The CMAF download opened the SecretBox (OS keyring -> secret-service -> a
synchronous zbus D-Bus round-trip) directly on a tokio async worker, which
panicked ("Cannot start a runtime from within a runtime") and poisoned the
vault mutex — every later attempt then failed and fell back to the legacy
plain-FLAC path. Playback never hit this because load_cmaf_bundle already runs
inside spawn_blocking.

- downloader: wrap the vault open + key wrap in tokio::task::spawn_blocking
  (off the async pool), mirroring the playback decrypt path.
- secret_vault: get_or_init recovers a poisoned mutex (into_inner) instead of
  wedging the vault for the rest of the session.

Verified at runtime: CMAF bundle now persists (tracks-cmaf/<id>/) and plays
back decrypted; no panic, no legacy fallback.

cc
2026-05-28 08:28:46 -06:00
vicrodh c8158bf419 refactor(offline-cache): re-wire Tauri onto qbz-offline-cache; delete old module
Completes Slice 0. src-tauri now depends on the qbz-offline-cache crate; the
old src-tauri/src/offline_cache/ implementation files are deleted. What remains
in that module is a thin shim: a re-export of the crate plus the Tauri glue.

- offline_cache/mod.rs: pub use qbz_offline_cache::* + tauri_cache_sink (maps
  CacheEvent -> the exact legacy offline:caching_*/offline:unlock_* app.emit
  shapes, so the Svelte frontend is unchanged) + core_client (resolves the core
  qbz_qobuz::QobuzClient from the CoreBridge for the download pipeline).
- Cache/redownload/migration commands: drop the legacy crate::api::QobuzClient
  (state.client) + the CoreBridge double-plumb; pass one core client + an
  app.emit-backed CacheEventSink. Unused `state: State<AppState>` params dropped.
- Playback + library cached-resolution call sites: pass &tauri_cache_sink(..)
  to load_cmaf_bundle_with_ui_events instead of &app_handle.
- session_lifecycle: apply_persisted_limit now reads the persisted value here;
  purge takes &library.db directly.
- migration: qobuz_client is the Option-wrapped core client (consistent with the
  downloader); None mid-loop is recorded as a per-track failure.

Full src-tauri cargo check passes; behaviour-preserving for the Svelte frontend.
Backend extraction (ADR-006) done — next is Slice 1 (shared tier-walk in core).

cc
2026-05-27 20:09:09 -06:00
vicrodh b17bbbd442 feat(offline-cache): move OfflineCacheState + purge into the crate
Completes the crate extraction (Slice 0 backend half):

- state: OfflineCacheState holder (db/fetcher/cache_dir/limit/semaphore/
  library_db + dir-layout helpers + new/init_at/teardown). apply_persisted_limit
  now takes Option<u64> (the caller reads it from offline_settings) instead of
  borrowing src-tauri's OfflineState, so the struct is frontend-agnostic and
  Slint can own one too.
- purge: purge_all_cached_files takes &Mutex<Option<LibraryDatabase>> directly
  instead of src-tauri's LibraryState wrapper.

The crate is now self-contained (13 modules, no Tauri). Next: re-wire the
Tauri commands onto it with an app.emit-backed CacheEventSink and delete the
old src-tauri/src/offline_cache/ module.

cc
2026-05-27 19:50:23 -06:00
vicrodh fc6bb7c559 feat(offline-cache): headless download pipeline via CacheEvent sink
Moves the download pipeline into the crate and removes its Tauri coupling:

- event: new CacheEvent enum (Started/Progress/Completed/Processed/Failed/
  UnlockStart/UnlockEnd) + CacheFormat + the CacheEventSink callback type.
- downloader: StreamFetcher + try_cmaf_offline_download + spawn_track_cache_download
  now report through a &CacheEventSink instead of tauri AppHandle::emit. The
  CoreBridge param is gone — both the CMAF raw download, metadata fetch, and the
  legacy stream-url fallback unify on a single core qbz_qobuz::QobuzClient handle.
  TLS uses the workspace rustls (same backend qbz-qobuz uses for all CDN traffic).
- playback: adds load_cmaf_bundle_with_ui_events that emits UnlockStart/UnlockEnd
  through the sink (the pure load_cmaf_bundle is unchanged).

Crate compiles headless; 5 unit tests pass. Remaining for Slice 0:
OfflineCacheState + purge, then re-wire the Tauri commands with an
app.emit-backed sink and delete the old src-tauri module.

cc
2026-05-27 19:44:21 -06:00
vicrodh 5f88201736 feat(offline-cache): move metadata/migration + playback pure-fn into the crate
Continues Slice 0. Moves the near-pure modules, re-pointing the src-tauri
client/library types to the core crates (the make-or-break for a mechanical
extraction — verified the field shapes match):

- metadata: fetch_complete_metadata now takes &qbz_qobuz::QobuzClient (was
  crate::api::QobuzClient); body compiles unchanged against qbz_models.
- migration: crate::api::QobuzClient -> qbz_qobuz::QobuzClient,
  crate::library::database::LibraryDatabase -> qbz_library::LibraryDatabase,
  super::metadata -> crate::metadata.
- playback: the PURE load_cmaf_bundle fn (the UI-events wrapper stays out —
  it becomes a CacheEventSink call in the next commit).

Deps added: qbz-models, qbz-library, tokio, reqwest, lofty. Crate compiles.
purge + OfflineCacheState + downloader (CacheEvent) remain.

cc
2026-05-27 19:27:39 -06:00
vicrodh d6e9345f5a feat(offline-cache): scaffold qbz-offline-cache crate with the tauri-free core
Slice 0 of the offline-cache port (extraction per ADR-006). Creates the new
qbz-offline-cache workspace crate and moves the tauri-free leaf modules out of
src-tauri/src/offline_cache/ verbatim:

- types: the shared serde DTOs (was inline in offline_cache/mod.rs)
- db: the SQLite cached_tracks index (super:: -> crate::types)
- secret_vault, cmaf_store, path_validator: moved verbatim
- maintenance: super:: refs re-pointed to crate::

src-tauri is untouched and keeps its own copy, so the production build stays
green; the re-point + delete happens at the end of Slice 0. Crate compiles
in isolation and all 5 unit tests pass.

Remaining for Slice 0: playback pure-fn split, metadata/migration/purge
(re-point crate::api/crate::library -> qbz_qobuz/qbz_library), the downloader
+ CacheEvent/CacheEventSink abstraction, the OfflineCacheState struct, then
re-wire the Tauri commands and delete the old module.

cc
2026-05-27 19:08:52 -06:00