oxyromon 0.24.0

Rusty ROM OrgaNizer
use super::common::*;
use super::database::*;
use super::mimetype::*;
use super::model::*;
use super::progress::*;
use super::prompt::*;
use super::util::*;
use anyhow::Result;
use clap::value_parser;
use clap::{Arg, ArgAction, ArgMatches, Command};
use indicatif::ProgressBar;
use sqlx::sqlite::SqliteConnection;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use strum::{Display, EnumString};

#[derive(Clone, Copy, Display, EnumString, PartialEq, Eq)]
#[strum(serialize_all = "lowercase")]
pub enum PatchType {
    Bps,
    Ips,
    Xdelta,
}

pub fn subcommand() -> Command {
    Command::new("import-patches")
        .about("Import patch files into oxyromon")
        .arg(
            Arg::new("PATCHES")
                .help("Set the patch files to import")
                .required(true)
                .num_args(1..)
                .index(1)
                .value_parser(value_parser!(PathBuf)),
        )
        .arg(
            Arg::new("NAME")
                .short('n')
                .long("name")
                .help("Customize patch names")
                .required(false)
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("FORCE")
                .short('f')
                .long("force")
                .help("Force import of already imported patch files")
                .required(false)
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("ROM")
                .long("rom")
                .help("Select the target ROM by id, skipping the prompts")
                .required(false)
                .num_args(1)
                .value_parser(value_parser!(i64)),
        )
}

pub async fn main(
    connection: &mut SqliteConnection,
    matches: &ArgMatches,
    progress_bar: &ProgressBar,
) -> Result<()> {
    let patch_paths: Vec<&PathBuf> = matches.get_many::<PathBuf>("PATCHES").unwrap().collect();
    let name = matches.get_flag("NAME");
    let force = matches.get_flag("FORCE");
    let rom_id = matches.get_one::<i64>("ROM").copied();
    for patch_path in patch_paths {
        match parse_patch(patch_path).await? {
            Some(patch_format) => {
                import_patch(
                    connection,
                    progress_bar,
                    patch_path,
                    &patch_format,
                    name,
                    force,
                    rom_id,
                )
                .await?;
            }
            _ => {
                print_warning(progress_bar, "Unsupported patch format");
            }
        }
        print_separator(progress_bar);
    }
    Ok(())
}

pub async fn parse_patch<P: AsRef<Path>>(path: &P) -> Result<Option<PatchType>> {
    let mimetype = get_mimetype(path).await?;
    Ok(match mimetype {
        Some(mimetype) => PatchType::from_str(mimetype.extension()).ok(),
        None => None,
    })
}

pub async fn import_patch<P: AsRef<Path>>(
    connection: &mut SqliteConnection,
    progress_bar: &ProgressBar,
    patch_path: &P,
    patch_format: &PatchType,
    name: bool,
    force: bool,
    rom_id: Option<i64>,
) -> Result<()> {
    // When the target ROM is known it is used directly; otherwise the
    // interactive prompts pick it.
    match rom_id {
        Some(rom_id) => {
            let rom = find_rom_by_id_opt(connection, rom_id)
                .await
                .ok_or_else(|| anyhow::anyhow!("ROM with id {} not found", rom_id))?;
            do_import_patch(
                connection,
                progress_bar,
                patch_path,
                patch_format,
                name,
                force,
                &rom,
            )
            .await
        }
        None => {
            let system = prompt_for_system(connection, None).await?;
            let games = find_full_games_by_system_id(connection, system.id).await;
            let game = match prompt_for_game(&games, None)? {
                Some(game) => game,
                None => {
                    print_skip(progress_bar, "Skipping patch");
                    return Ok(());
                }
            };
            let roms = find_roms_by_game_id_no_parents(connection, game.id).await;
            let rom = match prompt_for_rom(&roms, None)? {
                Some(rom) => rom,
                None => {
                    print_skip(progress_bar, "Skipping patch");
                    return Ok(());
                }
            };
            do_import_patch(
                connection,
                progress_bar,
                patch_path,
                patch_format,
                name,
                force,
                rom,
            )
            .await
        }
    }
}

async fn do_import_patch<P: AsRef<Path>>(
    connection: &mut SqliteConnection,
    progress_bar: &ProgressBar,
    patch_path: &P,
    patch_format: &PatchType,
    name: bool,
    force: bool,
    rom: &Rom,
) -> Result<()> {
    let patch_name = match name {
        true => match editor("Please enter a name for the patch")? {
            Some(name) => name,
            None => {
                print_skip(progress_bar, "Skipping patch");
                return Ok(());
            }
        },
        false => patch_path
            .as_ref()
            .file_stem()
            .unwrap()
            .to_str()
            .unwrap()
            .to_string(),
    };

    let mut extension = match patch_format {
        PatchType::Bps => BPS_EXTENSION,
        PatchType::Ips => IPS_EXTENSION,
        PatchType::Xdelta => XDELTA_EXTENSION,
    }
    .to_string();

    let existing_patches = find_patches_by_rom_id(connection, rom.id).await;
    if !existing_patches.is_empty() {
        extension = format!("{}{}", extension, existing_patches.len());
    }

    let game = find_game_by_id(connection, rom.game_id).await;
    let system = find_system_by_id(connection, game.system_id).await;
    let mut romfile_path = if game.sorting == Sorting::OneRegion as i64 {
        get_one_region_directory(connection, &system).await?
    } else {
        get_system_directory(connection, &system).await?
    };
    romfile_path = romfile_path.join(&rom.name).with_extension(extension);

    if let Some(patch) = existing_patches
        .iter()
        .find(|patch| patch.name == patch_name)
    {
        if force {
            CommonRomfile::from_path(patch_path)?
                .rename(progress_bar, &romfile_path, false)
                .await?
                .update(connection, progress_bar, patch.romfile_id)
                .await?;
        } else {
            print_skip(progress_bar, "Name already exists, skipping patch");
        }
    } else {
        let mut transaction = begin_transaction(connection).await;
        let romfile_id = CommonRomfile::from_path(patch_path)?
            .rename(progress_bar, &romfile_path, false)
            .await?
            .create(&mut transaction, progress_bar, RomfileType::Romfile)
            .await?;
        create_patch(
            &mut transaction,
            &patch_name,
            existing_patches.len() as i64,
            rom.id,
            romfile_id,
        )
        .await;
        commit_transaction(transaction).await;
    }

    Ok(())
}

#[cfg(test)]
mod test_bps;
#[cfg(test)]
mod test_bps_by_rom_id;
#[cfg(test)]
mod test_bps_ips;
#[cfg(test)]
mod test_ips;
#[cfg(test)]
mod test_xdelta;