feat(qbzd): implement all wizard sections (cache, integrations, qconnect)

Cache: interactive RAM detection, L1/L2 size, prefetch config.
Saves to qbzd.toml via save_default_config.

Integrations: ListenBrainz token input, saves directly to V2
cache database. MusicBrainz enable/disable toggle.

QConnect: enable/disable + device name. Saves to qbzd.toml.

All sections read current values as defaults and persist changes.
No more placeholders.

cc
This commit is contained in:
vicrodh committed 2026-04-09 11:39:22 -06:00
1 parent c63799309f
commit 264302fd8e
4 files changed
+184 -21

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
//! Cache & Resources wizard section.
use crate::config::{DaemonConfig, save_default_config};
pub fn run_cache_wizard() -> Result<(), String> {
println!("\n=== Cache & Resources ===\n");
let sys = sysinfo::System::new_with_specifics(
sysinfo::RefreshKind::nothing().with_memory(sysinfo::MemoryRefreshKind::everything()),
);
let total_ram_mb = sys.total_memory() / (1024 * 1024);
let auto_cache = (total_ram_mb / 8).min(400).max(50);
println!("System RAM: {} MB", total_ram_mb);
println!("Recommended L1 cache: {} MB\n", auto_cache);
// Memory cache
println!("Memory cache (L1) in MB [0 = auto-detect, current recommended: {}]:", auto_cache);
let memory_mb = read_number_or_default(0);
// Disk cache
println!("Disk cache (L2) in MB [default: 400]:");
let disk_mb = read_number_or_default(400);
// Prefetch
println!("Prefetch tracks ahead [default: 2]:");
let prefetch_count = read_number_or_default(2);
println!("Concurrent prefetch downloads [default: 1]:");
let prefetch_concurrent = read_number_or_default(1);
println!("CMAF parallel segments [default: 2]:");
let cmaf_concurrent = read_number_or_default(2);
// Load existing config and update
let mut config = DaemonConfig::load(None);
config.cache.memory_mb = memory_mb;
config.cache.disk_mb = disk_mb;
config.cache.prefetch_count = prefetch_count;
config.cache.prefetch_concurrent = prefetch_concurrent;
config.cache.cmaf_concurrent_segments = cmaf_concurrent;
config.cache.auto.enabled = memory_mb == 0;
save_default_config(&config)?;
println!("\nCache settings saved.");
println!(" L1 memory: {} MB {}", memory_mb, if memory_mb == 0 { "(auto-detect)" } else { "" });
println!(" L2 disk: {} MB", disk_mb);
println!(" Prefetch: {} tracks, {} concurrent", prefetch_count, prefetch_concurrent);
println!(" CMAF segments: {} parallel\n", cmaf_concurrent);
Ok(())
}
fn read_number_or_default(default: usize) -> usize {
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap_or_default();
let trimmed = input.trim();
if trimmed.is_empty() {
default
} else {
trimmed.parse().unwrap_or(default)
}
}
+66
View File
@@ -0,0 +1,66 @@
//! Integrations wizard section.
pub fn run_integrations_wizard() -> Result<(), String> {
println!("\n=== Integrations ===\n");
// ListenBrainz
println!("ListenBrainz token (leave empty to skip):");
let mut lb_token = String::new();
std::io::stdin().read_line(&mut lb_token).unwrap_or_default();
let lb_token = lb_token.trim();
if !lb_token.is_empty() {
// Save to the V2 cache database
let data_dir = dirs::data_dir()
.ok_or("Cannot determine data directory")?
.join("qbz");
// Find the user's data dir (from last_user_id)
let user_dir = find_user_data_dir(&data_dir)?;
let cache_dir = user_dir.join("cache");
std::fs::create_dir_all(&cache_dir).ok();
let db_path = cache_dir.join("listenbrainz_v2.db");
let conn = rusqlite::Connection::open(&db_path)
.map_err(|e| format!("DB error: {}", e))?;
conn.execute_batch(
"PRAGMA journal_mode=WAL; \
CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT NOT NULL)"
).map_err(|e| format!("Schema error: {}", e))?;
conn.execute(
"INSERT OR REPLACE INTO config (key, value) VALUES ('token', ?1)",
rusqlite::params![lb_token],
).map_err(|e| format!("Save error: {}", e))?;
println!("ListenBrainz token saved.\n");
} else {
println!("ListenBrainz skipped.\n");
}
// MusicBrainz
println!("Enable MusicBrainz enrichment? [Y/n]:");
let mut mb_input = String::new();
std::io::stdin().read_line(&mut mb_input).unwrap_or_default();
let mb_enabled = !mb_input.trim().eq_ignore_ascii_case("n");
println!("MusicBrainz: {}\n", if mb_enabled { "enabled" } else { "disabled" });
println!("Integrations configured.\n");
Ok(())
}
fn find_user_data_dir(global_data: &std::path::Path) -> Result<std::path::PathBuf, String> {
// Read last_user_id
let marker = global_data.join("last_user_id");
if marker.exists() {
if let Ok(uid) = std::fs::read_to_string(&marker) {
let uid = uid.trim();
if !uid.is_empty() {
let user_dir = global_data.join("users").join(uid);
if user_dir.exists() {
return Ok(user_dir);
}
}
}
}
Err("No user session found. Run 'qbzd login' first.".to_string())
}
+6 -21
View File
@@ -5,6 +5,9 @@
//! databases directly (runs without the daemon).
mod audio;
mod cache;
mod integrations;
mod qconnect;
use crossterm::{
event::{self, Event, KeyCode},
@@ -128,27 +131,9 @@ fn draw_menu(frame: &mut Frame, selected: usize) {
fn run_section(section: &str) -> Result<(), String> {
match section {
"audio" => audio::run_audio_wizard(),
"cache" => {
println!("\n=== Cache & Resources ===");
println!("Edit qbzd.toml [cache] section to configure:");
println!(" memory_mb, disk_mb, prefetch_count, prefetch_concurrent");
println!(" Set memory_mb = 0 for auto-detect from RAM\n");
Ok(())
}
"integrations" => {
println!("\n=== Integrations ===");
println!("ListenBrainz: use the HTTP API to connect:");
println!(" POST /api/integrations/listenbrainz/connect");
println!(" body: {{\"token\": \"your-listenbrainz-token\"}}\n");
Ok(())
}
"qconnect" => {
println!("\n=== QConnect ===");
println!("Edit qbzd.toml [qconnect] section:");
println!(" enabled = true/false");
println!(" device_name = \"your-device-name\"\n");
Ok(())
}
"cache" => cache::run_cache_wizard(),
"integrations" => integrations::run_integrations_wizard(),
"qconnect" => qconnect::run_qconnect_wizard(),
_ => Err(format!("Unknown section: {}", section)),
}
}
+49
View File
@@ -0,0 +1,49 @@
//! QConnect wizard section.
use crate::config::{DaemonConfig, save_default_config};
pub fn run_qconnect_wizard() -> Result<(), String> {
println!("\n=== QConnect (Qobuz Connect) ===\n");
let mut config = DaemonConfig::load(None);
// Enable/disable
println!("Enable QConnect? [Y/n] (current: {}):", if config.qconnect.enabled { "enabled" } else { "disabled" });
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap_or_default();
let trimmed = input.trim();
if !trimmed.is_empty() {
config.qconnect.enabled = !trimmed.eq_ignore_ascii_case("n");
}
if config.qconnect.enabled {
// Device name
let current_name = if config.qconnect.device_name.is_empty() {
hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_else(|| "qbzd".to_string())
} else {
config.qconnect.device_name.clone()
};
println!("Device name [current: {}]:", current_name);
let mut name_input = String::new();
std::io::stdin().read_line(&mut name_input).unwrap_or_default();
let name_trimmed = name_input.trim();
if !name_trimmed.is_empty() {
config.qconnect.device_name = name_trimmed.to_string();
}
}
save_default_config(&config)?;
println!("\nQConnect settings saved.");
println!(" Enabled: {}", config.qconnect.enabled);
if config.qconnect.enabled {
let name = if config.qconnect.device_name.is_empty() { "hostname" } else { &config.qconnect.device_name };
println!(" Device name: {}", name);
}
println!();
Ok(())
}