feat(core): shared offline-tier resolver (Slice 1 foundation)
Adds qbz_core::offline_resolve::resolve_offline_bytes — the OFFLINE tier of the playback tier-walk, frontend-agnostic and player-untouched (the user's locked Slice-1 decision): resolve a track's offline-cached bytes by Qobuz id (v2 CMAF decrypt via the secret vault, optionally emitting UnlockStart/End through a sink; v1 plain-FLAC read; None on miss). qbz-core now depends on qbz-offline-cache (aligns with the long-intended `offline` feature seam). Both frontends will slot this in before the network tier: Tauri keeps its hardware/quality gate around the returned bytes; Slint calls it before Player::play_track. Player's L1/L2 + network path is unchanged. Next: QbzCore compose methods (play_track_resolved / gapless), wire Slint's session to construct + own an OfflineCacheState, route the Slint play path through the resolver, and point Tauri's offline branch at it. cc
This commit is contained in:
4 files changed
+74
No files matched your search
Generated
+1
@@ -5058,6 +5058,7 @@ dependencies = [
|
||||
"qbz-audio",
|
||||
"qbz-integrations",
|
||||
"qbz-models",
|
||||
"qbz-offline-cache",
|
||||
"qbz-player",
|
||||
"qbz-qobuz",
|
||||
"qbz-radio",
|
||||
|
||||
@@ -12,6 +12,7 @@ qbz-player = { path = "../qbz-player" }
|
||||
qbz-qobuz = { path = "../qbz-qobuz" }
|
||||
qbz-radio = { path = "../qbz-radio" }
|
||||
qbz-integrations = { path = "../qbz-integrations" }
|
||||
qbz-offline-cache = { path = "../qbz-offline-cache" }
|
||||
|
||||
# Async
|
||||
tokio = { workspace = true }
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
|
||||
pub mod core;
|
||||
pub mod error;
|
||||
pub mod offline_resolve;
|
||||
pub mod system_capabilities;
|
||||
|
||||
// Re-exports from qbz-models for convenience
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//! Shared offline-cache → bytes resolver (frontend-agnostic).
|
||||
//!
|
||||
//! This is the OFFLINE tier of the playback tier-walk. Both frontends slot
|
||||
//! it in BEFORE the network tier in their play path; `qbz-player` (which
|
||||
//! already owns L1/L2 + the network/CMAF pipeline) stays untouched.
|
||||
//!
|
||||
//! - Tauri's `v2_play_track` / `v2_play_next_gapless` resolve offline bytes
|
||||
//! through this helper (keeping their hardware-compatibility / quality
|
||||
//! gating around the returned bytes).
|
||||
//! - Slint's play path calls it before `Player::play_track`.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use qbz_offline_cache::{CacheEventSink, OfflineCacheState};
|
||||
|
||||
/// Resolve a track's offline-cached bytes by its **Qobuz track id**.
|
||||
///
|
||||
/// - `cache_format = 2` (CMAF) → read the bundle, unwrap the content key via
|
||||
/// the secret vault, and decrypt to a complete FLAC (off the async thread).
|
||||
/// When `sink` is `Some`, `UnlockStart`/`UnlockEnd` are emitted around the
|
||||
/// decrypt so a frontend can show an "unlocking" hint on the row.
|
||||
/// - `cache_format = 1` (legacy plain FLAC) → read the file directly.
|
||||
/// - no ready row / any failure → `None` (the caller treats it as a cache
|
||||
/// miss and continues to the next tier / the network).
|
||||
///
|
||||
/// Offline copies are always downloaded at the top quality tier, so there is
|
||||
/// no quality gate here; a frontend that must honor a hardware limit (e.g.
|
||||
/// the Tauri ALSA path) applies its own check to the returned bytes.
|
||||
pub async fn resolve_offline_bytes(
|
||||
track_id: u64,
|
||||
offline: &OfflineCacheState,
|
||||
sink: Option<&CacheEventSink>,
|
||||
) -> Option<Vec<u8>> {
|
||||
let row = {
|
||||
let guard = offline.db.lock().await;
|
||||
guard.as_ref()?.get_cmaf_bundle(track_id).ok().flatten()?
|
||||
};
|
||||
|
||||
match row.cache_format {
|
||||
2 => {
|
||||
let cache_path = offline.get_cache_path();
|
||||
match sink {
|
||||
Some(sink) => {
|
||||
qbz_offline_cache::load_cmaf_bundle_with_ui_events(
|
||||
sink, track_id, track_id, row, cache_path,
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => tokio::task::spawn_blocking(move || {
|
||||
qbz_offline_cache::load_cmaf_bundle(track_id, &row, Path::new(&cache_path))
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
.flatten(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let path = Path::new(&row.segments_path);
|
||||
if path.exists() {
|
||||
std::fs::read(path).ok()
|
||||
} else {
|
||||
log::warn!(
|
||||
"[OfflineResolve] track {} v1 row points to a missing file {:?}",
|
||||
track_id,
|
||||
path
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user