koan-cli 0.25.2

CLI entry point for koan — bit-perfect music player.
use koan_core::config;
use owo_colors::OwoColorize;

use super::open_db;

pub fn cmd_remote_login(url: &str, username: &str) {
    if !url.starts_with("https://") && !url.contains("localhost") && !url.contains("127.0.0.1") {
        eprintln!("warning: server URL does not use HTTPS — credentials will be sent in plaintext");
    }

    let password = rpassword::prompt_password("password: ").unwrap_or_else(|e| {
        eprintln!("{} {}", "error:".red().bold(), e);
        std::process::exit(1);
    });

    let client = koan_core::remote::client::SubsonicClient::new(url, username, &password);
    match client.ping() {
        Ok(()) => println!("{} {}", "connected".green(), url),
        Err(e) => {
            eprintln!("{} {}", "connection failed:".red().bold(), e);
            std::process::exit(1);
        }
    }

    // Patch only the [remote] section in config.local.toml — don't touch other sections.
    let mut remote_vals = toml::map::Map::new();
    remote_vals.insert("enabled".into(), toml::Value::Boolean(true));
    remote_vals.insert("url".into(), toml::Value::String(url.to_string()));
    remote_vals.insert("username".into(), toml::Value::String(username.to_string()));
    remote_vals.insert("password".into(), toml::Value::String(password));
    if let Err(e) = config::Config::patch_local("remote", &remote_vals) {
        eprintln!("{} {}", "config error:".red().bold(), e);
        std::process::exit(1);
    }
    println!("{}", "credentials saved to config.local.toml".green());
}

pub fn cmd_remote_sync(full: bool) {
    let cfg = config::Config::load().unwrap_or_default();
    let client = match koan_core::helpers::subsonic_client(&cfg) {
        Some(c) => c,
        None => {
            eprintln!(
                "{} no remote server configured — run {} first",
                "error:".red().bold(),
                "koan remote login".bold()
            );
            std::process::exit(1);
        }
    };

    let db = open_db();
    let start = std::time::Instant::now();

    match koan_core::remote::sync::sync_library(
        &db,
        &client,
        full,
        &cfg.remote.url,
        &cfg.remote.username,
    ) {
        Ok(result) => {
            let elapsed = start.elapsed();
            let headline = if result.is_complete() {
                "sync complete".green().bold().to_string()
            } else {
                "sync incomplete".yellow().bold().to_string()
            };
            println!(
                "{} {} {} artists, {} albums, {} tracks",
                headline,
                format!("({:.1}s)", elapsed.as_secs_f64()).dimmed(),
                result.artists_synced.to_string().bold(),
                result.albums_synced.to_string().bold(),
                result.tracks_synced.to_string().bold(),
            );
            if !result.is_complete() {
                eprintln!(
                    "{} {} album(s) could not be fetched — the sync watermark was left \
                     unchanged, so the next sync will retry them",
                    "warning:".yellow().bold(),
                    result.albums_failed.to_string().bold(),
                );
            }
        }
        Err(e) => {
            eprintln!("{} {}", "sync failed:".red().bold(), e);
            std::process::exit(1);
        }
    }

    // Sync favourites: push local → remote, pull remote → local.
    print!("{}", "syncing favourites...".dimmed());
    use std::io::Write;
    std::io::stdout().flush().ok();

    // Push: star any local favourites that have a remote_id.
    let local_favs =
        koan_core::db::queries::favourites_with_remote_id(&db.conn).unwrap_or_default();
    let mut starred = 0;
    for (_path, remote_id) in &local_favs {
        if client.star(remote_id).is_ok() {
            starred += 1;
        }
    }

    // Pull: import remote starred songs as local favourites.
    let imported = match client.get_starred() {
        Ok(songs) => {
            let remote_ids: Vec<String> = songs.into_iter().map(|s| s.id).collect();
            match koan_core::db::queries::import_remote_favourites(&db.conn, &remote_ids) {
                Ok(n) => n,
                Err(e) => {
                    eprintln!("\n{} importing favourites: {}", "error".red(), e);
                    0
                }
            }
        }
        Err(e) => {
            eprintln!("\n{} fetching starred: {}", "error".red(), e);
            0
        }
    };

    println!(
        "\r{} {} pushed, {} imported",
        "favourites synced:".green().bold(),
        starred.to_string().bold(),
        imported.to_string().bold(),
    );
}

pub fn cmd_remote_status() {
    let cfg = config::Config::load().unwrap_or_default();
    if !cfg.remote.enabled || cfg.remote.url.is_empty() {
        println!("no remote server configured");
        return;
    }

    println!("{} {}", "server:".cyan(), cfg.remote.url);
    println!("{} {}", "username:".cyan(), cfg.remote.username);

    let has_password = !cfg.remote.password.is_empty();
    println!(
        "{} {}",
        "password:".cyan(),
        if has_password {
            "configured".green().to_string()
        } else {
            "not set".red().to_string()
        }
    );

    if has_password && let Some(client) = koan_core::helpers::subsonic_client(&cfg) {
        match client.ping() {
            Ok(()) => println!("{} {}", "status:".cyan(), "connected".green()),
            Err(e) => println!(
                "{} {} {}",
                "status:".cyan(),
                "error".red(),
                format!("\u{2014} {}", e).dimmed()
            ),
        }
    }
}