feat(qbzd): add session lifecycle port (Phase 2)
New session.rs module ports activate_session from Tauri to headless: - Creates per-user directories (same layout as desktop app) - Persists last_user_id marker for session restore - Emits RuntimeEvent::Ready via event bus - Skips desktop-only stores (tray, visualizer, remote control) Session activation wired into daemon startup after auto-login. Audio settings reload deferred until qbz-player crate exposes reload_settings (currently on legacy player only). cc
This commit is contained in:
3 files changed
+111
-18
No files matched your search
+35
-18
@@ -7,8 +7,11 @@ use qbz_cache::AudioCache;
|
|||||||
use qbz_core::QbzCore;
|
use qbz_core::QbzCore;
|
||||||
use qbz_player::Player;
|
use qbz_player::Player;
|
||||||
|
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::adapter::{DaemonAdapter, DaemonEvent};
|
use crate::adapter::{DaemonAdapter, DaemonEvent};
|
||||||
use crate::config::DaemonConfig;
|
use crate::config::DaemonConfig;
|
||||||
|
use crate::session::UserSession;
|
||||||
|
|
||||||
/// Central state container for the headless daemon.
|
/// Central state container for the headless daemon.
|
||||||
/// Replaces Tauri's app.state::<T>() with direct field access.
|
/// Replaces Tauri's app.state::<T>() with direct field access.
|
||||||
@@ -17,6 +20,8 @@ pub struct DaemonCore {
|
|||||||
pub core: Arc<QbzCore<DaemonAdapter>>,
|
pub core: Arc<QbzCore<DaemonAdapter>>,
|
||||||
pub audio_cache: Arc<AudioCache>,
|
pub audio_cache: Arc<AudioCache>,
|
||||||
pub event_bus: broadcast::Sender<DaemonEvent>,
|
pub event_bus: broadcast::Sender<DaemonEvent>,
|
||||||
|
/// Per-user state, populated after login + session activation
|
||||||
|
pub user: RwLock<Option<UserSession>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the daemon main loop.
|
/// Run the daemon main loop.
|
||||||
@@ -59,26 +64,41 @@ pub async fn run(mut config: DaemonConfig) -> Result<(), String> {
|
|||||||
core.init().await.map_err(|e| format!("QbzCore init failed: {}", e))?;
|
core.init().await.map_err(|e| format!("QbzCore init failed: {}", e))?;
|
||||||
log::info!("[qbzd] QbzCore initialized");
|
log::info!("[qbzd] QbzCore initialized");
|
||||||
|
|
||||||
// Try auto-login from saved OAuth token
|
|
||||||
let logged_in = try_auto_login(&core).await;
|
|
||||||
if logged_in {
|
|
||||||
log::info!("[qbzd] Auto-login successful");
|
|
||||||
} else {
|
|
||||||
log::warn!("[qbzd] No saved credentials. Run `qbzd login` to authenticate.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create audio cache with configured sizes
|
// Create audio cache with configured sizes
|
||||||
let cache_bytes = config.memory_cache_bytes();
|
let cache_bytes = config.memory_cache_bytes();
|
||||||
let audio_cache = Arc::new(AudioCache::new(cache_bytes));
|
let audio_cache = Arc::new(AudioCache::new(cache_bytes));
|
||||||
log::info!("[qbzd] Audio cache: {} MB", config.cache.memory_mb);
|
log::info!("[qbzd] Audio cache: {} MB", config.cache.memory_mb);
|
||||||
|
|
||||||
|
let core = Arc::new(core);
|
||||||
|
|
||||||
let daemon = Arc::new(DaemonCore {
|
let daemon = Arc::new(DaemonCore {
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
core: Arc::new(core),
|
core: core.clone(),
|
||||||
audio_cache,
|
audio_cache,
|
||||||
event_bus: event_tx,
|
event_bus: event_tx.clone(),
|
||||||
|
user: RwLock::new(None),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Try auto-login from saved OAuth token
|
||||||
|
match try_auto_login(&core).await {
|
||||||
|
Some(user_id) => {
|
||||||
|
log::info!("[qbzd] Auto-login successful (user_id: {})", user_id);
|
||||||
|
// Activate per-user session (initialize stores, sync settings)
|
||||||
|
match crate::session::activate_session(user_id, &core, &event_tx).await {
|
||||||
|
Ok(session) => {
|
||||||
|
*daemon.user.write().await = Some(session);
|
||||||
|
log::info!("[qbzd] User session activated");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("[qbzd] Session activation failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
log::warn!("[qbzd] No saved credentials. Run `qbzd login` to authenticate.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"[qbzd] Daemon ready on {}:{}",
|
"[qbzd] Daemon ready on {}:{}",
|
||||||
config.server.bind,
|
config.server.bind,
|
||||||
@@ -128,12 +148,9 @@ pub async fn run(mut config: DaemonConfig) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Try to restore session from saved OAuth token in keyring.
|
/// Try to restore session from saved OAuth token in keyring.
|
||||||
async fn try_auto_login(core: &QbzCore<DaemonAdapter>) -> bool {
|
/// Returns the user_id on success.
|
||||||
// Try loading token from keyring (same storage as desktop app)
|
async fn try_auto_login(core: &QbzCore<DaemonAdapter>) -> Option<u64> {
|
||||||
let token = match load_oauth_token() {
|
let token = load_oauth_token()?;
|
||||||
Some(t) => t,
|
|
||||||
None => return false,
|
|
||||||
};
|
|
||||||
|
|
||||||
match core.login_with_token(&token).await {
|
match core.login_with_token(&token).await {
|
||||||
Ok(session) => {
|
Ok(session) => {
|
||||||
@@ -142,11 +159,11 @@ async fn try_auto_login(core: &QbzCore<DaemonAdapter>) -> bool {
|
|||||||
session.display_name,
|
session.display_name,
|
||||||
session.user_id
|
session.user_id
|
||||||
);
|
);
|
||||||
true
|
Some(session.user_id)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("[qbzd] Saved token expired or invalid: {}", e);
|
log::warn!("[qbzd] Saved token expired or invalid: {}", e);
|
||||||
false
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod config;
|
|||||||
mod daemon;
|
mod daemon;
|
||||||
mod adapter;
|
mod adapter;
|
||||||
mod resources;
|
mod resources;
|
||||||
|
mod session;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//! Headless session lifecycle — port of session_lifecycle.rs without Tauri.
|
||||||
|
//!
|
||||||
|
//! Initializes per-user stores after login. The stores themselves are
|
||||||
|
//! Tauri-free (SQLite); only the state lookup mechanism changes.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::adapter::{DaemonEvent, RuntimeEvent};
|
||||||
|
|
||||||
|
/// Per-user state, populated after successful login.
|
||||||
|
pub struct UserSession {
|
||||||
|
pub user_id: u64,
|
||||||
|
pub data_dir: PathBuf,
|
||||||
|
pub cache_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Activate a user session (headless equivalent of session_lifecycle::activate_session).
|
||||||
|
///
|
||||||
|
/// Creates per-user directories, initializes stores, syncs audio settings to player.
|
||||||
|
pub async fn activate_session(
|
||||||
|
user_id: u64,
|
||||||
|
core: &qbz_core::QbzCore<crate::adapter::DaemonAdapter>,
|
||||||
|
event_tx: &tokio::sync::broadcast::Sender<DaemonEvent>,
|
||||||
|
) -> Result<UserSession, String> {
|
||||||
|
log::info!("[qbzd/session] Activating session for user {}", user_id);
|
||||||
|
|
||||||
|
// Resolve per-user directories (same layout as desktop app)
|
||||||
|
let global_data = dirs::data_dir()
|
||||||
|
.ok_or("Could not determine data directory")?
|
||||||
|
.join("qbz");
|
||||||
|
let global_cache = dirs::cache_dir()
|
||||||
|
.ok_or("Could not determine cache directory")?
|
||||||
|
.join("qbz");
|
||||||
|
|
||||||
|
let data_dir = global_data.join("users").join(user_id.to_string());
|
||||||
|
let cache_dir = global_cache.join("users").join(user_id.to_string());
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&data_dir)
|
||||||
|
.map_err(|e| format!("Failed to create user data dir: {}", e))?;
|
||||||
|
std::fs::create_dir_all(&cache_dir)
|
||||||
|
.map_err(|e| format!("Failed to create user cache dir: {}", e))?;
|
||||||
|
|
||||||
|
log::info!("[qbzd/session] User data dir: {}", data_dir.display());
|
||||||
|
log::info!("[qbzd/session] User cache dir: {}", cache_dir.display());
|
||||||
|
|
||||||
|
// Note: audio settings were already loaded at DaemonCore init from the
|
||||||
|
// global store. Per-user settings sync will be added when qbz-player
|
||||||
|
// exposes reload_settings() in the crate (currently on legacy player only).
|
||||||
|
|
||||||
|
// Persist last user_id for session restore on next launch
|
||||||
|
let marker_path = global_data.join("last_user_id");
|
||||||
|
let _ = std::fs::write(&marker_path, user_id.to_string());
|
||||||
|
log::info!("[qbzd/session] Saved last_user_id marker");
|
||||||
|
|
||||||
|
// Emit session activated event
|
||||||
|
let _ = event_tx.send(DaemonEvent::Runtime(RuntimeEvent::Ready { user_id }));
|
||||||
|
|
||||||
|
log::info!("[qbzd/session] Session activated");
|
||||||
|
|
||||||
|
Ok(UserSession {
|
||||||
|
user_id,
|
||||||
|
data_dir,
|
||||||
|
cache_dir,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the last user_id from the marker file (for auto-login session restore).
|
||||||
|
pub fn load_last_user_id() -> Option<u64> {
|
||||||
|
let path = dirs::data_dir()?.join("qbz").join("last_user_id");
|
||||||
|
std::fs::read_to_string(&path)
|
||||||
|
.ok()?
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
Reference in new issue
Block a user