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:
4 files changed
+184
-21
No files matched your search
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
//! databases directly (runs without the daemon).
|
//! databases directly (runs without the daemon).
|
||||||
|
|
||||||
mod audio;
|
mod audio;
|
||||||
|
mod cache;
|
||||||
|
mod integrations;
|
||||||
|
mod qconnect;
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, Event, KeyCode},
|
event::{self, Event, KeyCode},
|
||||||
@@ -128,27 +131,9 @@ fn draw_menu(frame: &mut Frame, selected: usize) {
|
|||||||
fn run_section(section: &str) -> Result<(), String> {
|
fn run_section(section: &str) -> Result<(), String> {
|
||||||
match section {
|
match section {
|
||||||
"audio" => audio::run_audio_wizard(),
|
"audio" => audio::run_audio_wizard(),
|
||||||
"cache" => {
|
"cache" => cache::run_cache_wizard(),
|
||||||
println!("\n=== Cache & Resources ===");
|
"integrations" => integrations::run_integrations_wizard(),
|
||||||
println!("Edit qbzd.toml [cache] section to configure:");
|
"qconnect" => qconnect::run_qconnect_wizard(),
|
||||||
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(())
|
|
||||||
}
|
|
||||||
_ => Err(format!("Unknown section: {}", section)),
|
_ => Err(format!("Unknown section: {}", section)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
Reference in new issue
Block a user