osynic_serializer 0.1.3

A osu beatmapsets serializer lib & CLI application based on osynic_osudb
Documentation
#![cfg(feature = "cli")]

use clap::Parser;
use osynic_serializer::commands::{
    diff_sets, diff_songs, serialize_by_folder, serialize_by_osu_db,
};
use osynic_serializer::functions::check::{
    check_osu_dir, check_sets_type, check_songs_type, get_osu_dir,
};
use osynic_serializer::functions::parse::parse_song_id_list_with_mapper;
use osynic_serializer::functions::storage::marked_save_to;
use osynic_serializer::types::{Beatmapsets, SongWithMapper, SongsWithMapper};
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum Algorithm {
    Osudb,
    Folder,
}

#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum JsonType {
    Songs,
    Sets,
}

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct CliArgs {
    #[arg(short, long)]
    algorithm: Option<Algorithm>,

    #[arg(short = 't', long)]
    json_type: Option<JsonType>,

    #[arg(short, long)]
    path: Option<PathBuf>,

    #[arg(short, long)]
    diff: Option<PathBuf>,

    #[arg(short, long)]
    output: Option<PathBuf>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut args = CliArgs::parse();

    // Interactive mode: prompt for missing parameters
    println!("🎵 OsynicSerializer - Interactive Mode\n");

    // Select JSON type
    if args.json_type.is_none() {
        args.json_type = Some(prompt_json_type()?);
    }

    // Select algorithm
    if args.algorithm.is_none() {
        args.algorithm = Some(prompt_algorithm()?);
    }

    // Select osu! path
    if args.path.is_none() {
        args.path = Some(prompt_path()?);
    }

    // Validate path
    let osu_dir = args.path.clone().ok_or("osu! path not found")?;

    // Validate diff file if provided
    validate_diff_file(&args.diff, &args.json_type.unwrap())?;

    // Serialize beatmaps
    let songs = (match args.algorithm.unwrap() {
        Algorithm::Osudb => serialize_by_osu_db(osu_dir.to_str().unwrap_or_default()),
        Algorithm::Folder => serialize_by_folder(osu_dir.to_str().unwrap_or_default()),
    })?;

    // Select output directory
    if args.output.is_none() {
        args.output = Some(prompt_output_path()?);
    }

    // Process diff if needed
    let is_diff = args.diff.is_some();
    let json_type = args.json_type.unwrap();

    match json_type {
        JsonType::Sets => {
            let sets = Beatmapsets {
                beatmapset_ids: parse_song_id_list_with_mapper(&songs.songs),
            };
            let result_data = process_diff_sets(sets, args.diff)?;
            println!(
                "\n✅ Total beatmapsets after diff: {}",
                result_data.beatmapset_ids.len()
            );
            save_sets_data(
                is_diff,
                &args.output.unwrap(),
                &result_data,
                args.algorithm.unwrap(),
            )?;
        }
        JsonType::Songs => {
            let result_data = process_diff_songs(songs, args.diff)?;
            println!("\n✅ Total songs after diff: {}", result_data.songs.len());
            save_songs_data(
                is_diff,
                &args.output.unwrap(),
                &result_data,
                args.algorithm.unwrap(),
            )?;
        }
    }

    Ok(())
}

fn prompt_json_type() -> Result<JsonType, Box<dyn std::error::Error>> {
    use inquire::Select;

    let options = vec!["songs", "sets"];
    let ans = Select::new("📋 Select output JSON type:", options).prompt()?;

    Ok(match ans {
        "songs" => JsonType::Songs,
        "sets" => JsonType::Sets,
        _ => JsonType::Songs,
    })
}

fn prompt_algorithm() -> Result<Algorithm, Box<dyn std::error::Error>> {
    use inquire::Select;

    let options = vec!["osudb (Database - Recommended)", "folder (Direct scan)"];
    let ans = Select::new("🔧 Select serialization algorithm:", options).prompt()?;

    Ok(match ans {
        "osudb (Database - Recommended)" => Algorithm::Osudb,
        "folder (Direct scan)" => Algorithm::Folder,
        _ => Algorithm::Osudb,
    })
}

fn prompt_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
    use inquire::Confirm;

    let osu_path_detected = check_osu_dir();

    if osu_path_detected {
        let detected_path = get_osu_dir();
        println!("\n📁 Detected osu! installation at: {}", detected_path);
        let use_detected = Confirm::new("Use this path?").with_default(true).prompt()?;

        if use_detected {
            return Ok(PathBuf::from(detected_path));
        }
    }

    // Manual input
    use inquire::Text;
    let path = Text::new("📁 Enter osu! installation path:").prompt()?;
    Ok(PathBuf::from(path))
}

fn prompt_output_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
    use inquire::Text;

    let default_path = "./output";
    let path = Text::new("📁 Enter output directory path:")
        .with_default(default_path)
        .prompt()?;

    Ok(PathBuf::from(path))
}

fn validate_diff_file(
    diff_path: &Option<PathBuf>,
    json_type: &JsonType,
) -> Result<(), Box<dyn std::error::Error>> {
    if let Some(path) = diff_path {
        let content = std::fs::read_to_string(path)?;
        let is_valid = match json_type {
            JsonType::Songs => check_songs_type(&content),
            JsonType::Sets => check_sets_type(&content),
        };

        if !is_valid {
            return Err("Invalid diff file".into());
        }
    }
    Ok(())
}

fn process_diff_sets(
    base_data: Beatmapsets,
    diff_path: Option<PathBuf>,
) -> Result<Beatmapsets, Box<dyn std::error::Error>> {
    match diff_path {
        Some(path) => {
            let diff_content = std::fs::read_to_string(path)?;
            let diff_data: Beatmapsets = serde_json::from_str(&diff_content)?;
            Ok(diff_sets(&base_data, &diff_data))
        }
        None => Ok(base_data),
    }
}

fn process_diff_songs(
    base_data: SongsWithMapper,
    diff_path: Option<PathBuf>,
) -> Result<SongsWithMapper, Box<dyn std::error::Error>> {
    match diff_path {
        Some(path) => {
            let diff_content = std::fs::read_to_string(path)?;
            let diff_data: Vec<SongWithMapper> = serde_json::from_str(&diff_content)?;
            let diff_data = SongsWithMapper { songs: diff_data };
            Ok(diff_songs(&base_data, &diff_data))
        }
        None => Ok(base_data),
    }
}

fn save_sets_data(
    is_diff: bool,
    output_dir: &Path,
    data: &Beatmapsets,
    algorithm: Algorithm,
) -> Result<(), Box<dyn std::error::Error>> {
    let diff_mark = if is_diff { "diff_" } else { "" };
    let filename = format!(
        "{}{}_{}.json",
        diff_mark,
        match algorithm {
            Algorithm::Osudb => "sets_dm",
            Algorithm::Folder => "sets_m",
        },
        chrono::Local::now().format("%y%m%d%H%M%S")
    );
    let json = serde_json::to_string_pretty(data)?;
    marked_save_to(output_dir.to_str().unwrap_or_default(), &filename, &json)?;
    println!("📄 Output file: {}/{}", output_dir.display(), filename);
    Ok(())
}

fn save_songs_data(
    is_diff: bool,
    output_dir: &Path,
    data: &SongsWithMapper,
    algorithm: Algorithm,
) -> Result<(), Box<dyn std::error::Error>> {
    let diff_mark = if is_diff { "diff_" } else { "" };
    let filename = format!(
        "{}{}_{}.json",
        diff_mark,
        match algorithm {
            Algorithm::Osudb => "songs_dm",
            Algorithm::Folder => "songs_m",
        },
        chrono::Local::now().format("%y%m%d%H%M%S")
    );
    let json = serde_json::to_string_pretty(&data.songs)?;
    marked_save_to(output_dir.to_str().unwrap_or_default(), &filename, &json)?;
    println!("📄 Output file: {}/{}", output_dir.display(), filename);
    Ok(())
}