use super::chdman;
use super::chdman::{AsChd, ChdType};
use super::common::*;
use super::config::*;
use super::ctrtool;
use super::database::*;
use super::dolphin;
use super::dolphin::AsRvz;
use super::maxcso;
use super::maxcso::AsXso;
use super::mimetype::*;
use super::model::*;
use super::nsz;
use super::nsz::AsNsz;
use super::progress::*;
use super::prompt::*;
use super::sevenzip;
use super::sevenzip::{ArchiveFile, AsArchive};
use super::util::*;
use anyhow::{Result, bail};
use clap::value_parser;
use clap::{Arg, ArgAction, ArgMatches, Command};
use indicatif::ProgressBar;
use rayon::prelude::*;
use sqlx::sqlite::SqliteConnection;
use std::cmp::Ordering;
use std::collections::HashSet;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use strsim::jaro_winkler;
use strum::{Display, IntoEnumIterator};
use walkdir::WalkDir;
#[derive(Display, PartialEq)]
enum MatchState {
Valid = 0,
Duplicate = 1,
Invalid = 2,
Skipped = 3,
}
#[derive(Clone, Copy, PartialEq)]
pub enum UnattendedMode {
None,
Skip,
First,
}
impl UnattendedMode {
pub fn from_str(s: &str) -> Self {
match s {
"skip" => UnattendedMode::Skip,
"first" => UnattendedMode::First,
_ => UnattendedMode::None,
}
}
}
struct MatchResult {
state: MatchState,
system: Option<System>,
game: Option<Game>,
rom: Option<Rom>,
hash_algorithm: Option<String>,
hash: Option<String>,
}
pub fn subcommand() -> Command {
Command::new("import-roms")
.about("Validate and import ROM files or directories into oxyromon")
.arg(
Arg::new("ROMS")
.help("Set the ROM files or directories to import")
.required(true)
.num_args(1..)
.index(1)
.value_parser(value_parser!(PathBuf)),
)
.arg(
Arg::new("SYSTEM")
.short('s')
.long("system")
.help("Select systems by name")
.required(false)
.action(ArgAction::Append),
)
.arg(
Arg::new("TRASH")
.short('t')
.long("trash")
.help("Trash invalid ROM files")
.required(false)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("DELETE")
.short('d')
.long("delete")
.help("Delete invalid ROM files (hard delete)")
.required(false)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("FORCE")
.short('f')
.long("force")
.help("Force import of existing ROM files")
.required(false)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("UNATTENDED")
.short('u')
.long("unattended")
.help("Handle ROM files that require human intervention (skip: skip, first: auto-select first match)")
.required(false)
.value_name("MODE")
.default_value("none")
.value_parser(["none", "skip", "first"]),
)
.arg(
Arg::new("EXTRACT")
.short('x')
.long("extract")
.help("Extract top-level archives before importing their contents")
.required(false)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("AS_IS")
.short('a')
.long("as-is")
.help("Import ROM files as-is without converting them")
.required(false)
.action(ArgAction::SetTrue),
)
}
pub async fn main(
connection: &mut SqliteConnection,
matches: &ArgMatches,
progress_bar: &ProgressBar,
) -> Result<()> {
let mut systems: Vec<System> = vec![];
if let Some(system_names) = matches.get_many::<String>("SYSTEM") {
for system_name in system_names {
systems.append(&mut find_systems_by_name_like(connection, system_name).await);
}
}
systems.dedup_by_key(|system| system.id);
let mut systems: Vec<Option<System>> = systems.into_iter().map(Some).collect();
if systems.is_empty() {
systems.push(None);
}
let trash = matches.get_flag("TRASH");
let delete = matches.get_flag("DELETE");
if trash && delete {
bail!("Cannot use both --trash and --delete flags simultaneously");
}
let force = matches.get_flag("FORCE");
let unattended_mode =
UnattendedMode::from_str(matches.get_one::<String>("UNATTENDED").unwrap().as_str());
let as_is = matches.get_flag("AS_IS");
let mut system_ids: HashSet<i64> = HashSet::new();
let mut game_ids: HashSet<i64> = HashSet::new();
let paths: Vec<PathBuf> = matches
.get_many::<PathBuf>("ROMS")
.unwrap()
.cloned()
.collect();
let mut paths_with_priority: Vec<(PathBuf, i32)> = Vec::new();
for path in paths {
let priority = if path.is_file() {
if let Ok(Some(mimetype)) = get_mimetype(&path).await {
if mimetype.extension() == CHD_EXTENSION {
let romfile = CommonRomfile {
path: path.clone(),
system_id: None,
};
if let Ok((_, _, _, _, parent_sha1, _)) = romfile.parse_chd().await {
if parent_sha1.is_some() {
0 } else {
1 }
} else {
2 }
} else {
2 }
} else {
2 }
} else {
2 };
paths_with_priority.push((path, priority));
}
paths_with_priority.sort_by_key(|(_, priority)| *priority);
let sorted_paths: Vec<PathBuf> = paths_with_priority
.into_iter()
.map(|(path, _)| path)
.collect();
for path in sorted_paths {
let tmp_directory = create_tmp_directory(connection).await?;
let mut path = get_canonicalized_path(&path).await?;
if !path.is_dir() {
let mimetype = get_mimetype(&path).await?;
if matches.get_flag("EXTRACT")
&& mimetype.is_some()
&& ARCHIVE_EXTENSIONS.contains(&mimetype.as_ref().unwrap().extension())
{
for archive_romfile in CommonRomfile::from_path(&path)?
.as_archive(progress_bar, None)
.await?
{
archive_romfile
.to_common(progress_bar, &tmp_directory.path())
.await?;
}
path = tmp_directory.path().to_path_buf();
}
}
for system in &systems {
if let Some(system) = system {
print_header(progress_bar, &format!("Searching in \"{}\"", system.name));
}
let header = match system {
Some(system) => find_header_by_system_id(connection, system.id).await,
None => None,
};
if path.is_dir() {
if path.join(PS3_DISC_SFB).is_file() {
print_subheader(
progress_bar,
&format!(
"Processing \"{}\"",
path.file_name().unwrap().to_str().unwrap()
),
);
match system.as_ref() {
Some(system) => {
let system_id = import_jbfolder(
connection,
progress_bar,
system,
&path,
unattended_mode,
)
.await?;
system_ids.insert(system_id);
}
None => {
let system =
prompt_for_system_like(connection, None, "%PlayStation 3%").await?;
let system_id = import_jbfolder(
connection,
progress_bar,
&system,
&path,
unattended_mode,
)
.await?;
system_ids.insert(system_id);
}
}
} else {
let walker = WalkDir::new(&path).into_iter();
for entry in walker.filter_map(|e| e.ok()) {
if entry.path().is_file() {
let result = import_rom(
connection,
progress_bar,
&system.as_ref(),
&header,
&entry.path(),
trash,
delete,
force,
unattended_mode,
as_is,
)
.await;
let (new_system_ids, new_game_ids) = match result {
Ok(v) => v,
Err(e) => {
println!("{}\n\nContinuing", e);
continue;
}
};
system_ids.extend(new_system_ids);
game_ids.extend(new_game_ids);
}
}
}
} else {
let (new_system_ids, new_game_ids) = import_rom(
connection,
progress_bar,
&system.as_ref(),
&header,
&path,
trash,
delete,
force,
unattended_mode,
as_is,
)
.await?;
system_ids.extend(new_system_ids);
game_ids.extend(new_game_ids);
}
print_separator(progress_bar);
}
}
for system_id in system_ids {
let system = find_system_by_id(connection, system_id).await;
compute_system_completion(connection, progress_bar, &system).await?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn import_rom<P: AsRef<Path>>(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &Option<&System>,
header: &Option<Header>,
path: &P,
trash: bool,
delete: bool,
force: bool,
unattended_mode: UnattendedMode,
as_is: bool,
) -> Result<(HashSet<i64>, HashSet<i64>)> {
print_subheader(
progress_bar,
&format!(
"Processing \"{}\"",
path.as_ref().file_name().unwrap().to_str().unwrap()
),
);
let mut transaction = begin_transaction(connection).await;
let mut system_ids: HashSet<i64> = HashSet::new();
let mut game_ids: HashSet<i64> = HashSet::new();
let romfile = CommonRomfile::from_path(path)?;
let size = romfile.get_size(&mut transaction, progress_bar).await?;
if size == 0 {
print_skip(progress_bar, "Empty file, deleting");
romfile.delete(progress_bar, false).await?;
return Ok((system_ids, game_ids));
}
if let Ok(relative_path) = romfile.get_relative_path(&mut transaction).await
&& !force
&& find_romfile_by_path(
&mut transaction,
relative_path.as_os_str().to_str().unwrap(),
)
.await
.is_some()
{
print_skip(progress_bar, "Already in database");
return Ok((system_ids, game_ids));
}
let mimetype = get_mimetype(&romfile.path).await?;
let extension = match mimetype {
Some(mimetype) => mimetype.extension().to_string(),
None => romfile
.path
.extension()
.unwrap_or(&OsString::new())
.to_str()
.unwrap()
.to_lowercase(),
};
if ARCHIVE_EXTENSIONS.contains(&extension.as_str()) && !as_is {
if sevenzip::get_version().await.is_err() {
print_error(progress_bar, "Required tool not found: sevenzip");
return Ok((system_ids, game_ids));
}
let (new_system_ids, new_game_ids) = import_archive(
&mut transaction,
progress_bar,
system,
header,
&game_ids,
romfile,
&extension,
trash,
delete,
unattended_mode,
)
.await?;
system_ids.extend(new_system_ids);
game_ids.extend(new_game_ids);
} else if CHD_EXTENSION == extension && !as_is {
if chdman::get_version().await.is_err() {
print_error(progress_bar, "Required tool not found: chdman");
return Ok((system_ids, game_ids));
}
if let Some(ids) = import_chd(
&mut transaction,
progress_bar,
system,
&game_ids,
romfile,
trash,
delete,
unattended_mode,
)
.await?
{
system_ids.insert(ids[0]);
game_ids.insert(ids[1]);
};
} else if CIA_EXTENSION == extension && !as_is {
if ctrtool::get_version().await.is_err() {
print_error(progress_bar, "Required tool not found: ctrtool");
return Ok((system_ids, game_ids));
}
let (new_system_ids, new_game_ids) = import_cia(
&mut transaction,
progress_bar,
system,
&game_ids,
romfile,
trash,
delete,
unattended_mode,
)
.await?;
system_ids.extend(new_system_ids);
game_ids.extend(new_game_ids);
} else if CSO_EXTENSION == extension && !as_is {
if maxcso::get_version().await.is_err() {
print_error(progress_bar, "Required tool not found: maxcso");
return Ok((system_ids, game_ids));
}
if let Some(ids) = import_cso(
&mut transaction,
progress_bar,
system,
&game_ids,
romfile,
trash,
delete,
unattended_mode,
)
.await?
{
system_ids.insert(ids[0]);
game_ids.insert(ids[1]);
};
} else if NSZ_EXTENSION == extension && !as_is {
if nsz::get_version().await.is_err() {
print_error(progress_bar, "Required tool not found: nsz");
return Ok((system_ids, game_ids));
}
if let Some(ids) = import_nsz(
&mut transaction,
progress_bar,
system,
&game_ids,
romfile,
trash,
delete,
unattended_mode,
)
.await?
{
system_ids.insert(ids[0]);
game_ids.insert(ids[1]);
};
} else if RVZ_EXTENSION == extension && !as_is {
if dolphin::get_version().await.is_err() {
print_error(progress_bar, "Required tool not found: dolphin-tool");
return Ok((system_ids, game_ids));
}
if let Some(ids) = import_rvz(
&mut transaction,
progress_bar,
system,
&game_ids,
romfile,
trash,
delete,
unattended_mode,
)
.await?
{
system_ids.insert(ids[0]);
game_ids.insert(ids[1]);
};
} else if ZSO_EXTENSION == extension && !as_is {
if maxcso::get_version().await.is_err() {
print_error(progress_bar, "Required tool not found: maxcso");
return Ok((system_ids, game_ids));
}
if let Some(ids) = import_zso(
&mut transaction,
progress_bar,
system,
&game_ids,
romfile,
trash,
delete,
unattended_mode,
)
.await?
{
system_ids.insert(ids[0]);
game_ids.insert(ids[1]);
};
} else if let Some(ids) = import_other(
&mut transaction,
progress_bar,
system,
header,
&game_ids,
romfile,
trash,
delete,
unattended_mode,
)
.await?
{
system_ids.insert(ids[0]);
game_ids.insert(ids[1]);
};
commit_transaction(transaction).await;
Ok((system_ids, game_ids))
}
async fn import_jbfolder<P: AsRef<Path>>(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &System,
path: &P,
unattended_mode: UnattendedMode,
) -> Result<i64> {
let sfb_romfile_path = path.as_ref().join(PS3_DISC_SFB);
if find_romfile_by_path(connection, sfb_romfile_path.as_os_str().to_str().unwrap())
.await
.is_some()
{
print_skip(progress_bar, "Already in database");
return Ok(system.id);
}
let mut transaction = begin_transaction(connection).await;
let original_romfile = CommonRomfile::from_path(&sfb_romfile_path)?;
let (md5, size) = original_romfile
.get_hash_and_size(&mut transaction, progress_bar, 1, 1, &HashAlgorithm::Md5)
.await?;
let sfb_match_result = find_sfb_rom_by_md5(
&mut transaction,
progress_bar,
system,
size,
&md5,
unattended_mode,
)
.await?;
match sfb_match_result.state {
MatchState::Valid => {
let system_directory = get_system_directory(&mut transaction, system).await?;
let game = sfb_match_result.game.unwrap();
let sfb_rom = sfb_match_result.rom.unwrap();
let walker = WalkDir::new(path.as_ref()).into_iter();
for entry in walker.filter_map(|e| e.ok()) {
if entry.path().is_file() {
print_action(
progress_bar,
&format!(
"Processing \"{}\"",
entry.path().as_os_str().to_str().unwrap()
),
);
let original_romfile = CommonRomfile::from_path(&entry.path())?;
let (md5, size) = original_romfile
.get_hash_and_size(
&mut transaction,
progress_bar,
1,
1,
&HashAlgorithm::Md5,
)
.await?;
let rom: Option<&Rom>;
let mut roms = find_roms_without_romfile_by_size_and_md5_and_parent_id(
&mut transaction,
size,
&md5,
sfb_rom.parent_id.unwrap(),
)
.await;
if roms.is_empty() {
if count_roms_with_romfile_by_size_and_md5_and_parent_id(
&mut transaction,
size,
&md5,
sfb_rom.parent_id.unwrap(),
)
.await
> 0
{
print_skip(progress_bar, "Already imported");
} else {
print_warning(progress_bar, "No match");
}
continue;
}
if roms.len() == 1 {
rom = roms.first();
print_success(
progress_bar,
&format!("Matches \"{}\"", rom.as_ref().unwrap().name),
);
} else if let Some(rom_index) = roms.iter().position(|rom| {
entry
.path()
.as_os_str()
.to_str()
.unwrap()
.ends_with(&rom.name)
}) {
rom = roms.get(rom_index);
print_success(
progress_bar,
&format!("Matches \"{}\"", rom.as_ref().unwrap().name),
);
} else if unattended_mode == UnattendedMode::Skip {
roms.sort_by(|a, b| {
let a_distance = jaro_winkler(
&entry
.path()
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_lowercase(),
&a.name.to_lowercase(),
);
let b_distance = jaro_winkler(
&entry
.path()
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_lowercase(),
&b.name.to_lowercase(),
);
a_distance.partial_cmp(&b_distance).unwrap()
});
rom = roms.first();
} else {
rom = prompt_for_rom(&roms, None)?;
}
if let Some(rom) = rom {
if let Some(romfile_id) = rom.romfile_id {
let romfile = find_romfile_by_id(&mut transaction, romfile_id).await;
print_skip(progress_bar, &format!("Duplicate of \"{}\"", romfile.path));
continue;
}
let new_path = system_directory.join(&game.name).join(&rom.name);
rename_file(progress_bar, &entry.path(), &new_path, false).await?;
create_or_update_romfile(
&mut transaction,
progress_bar,
&new_path,
&[rom],
Some(system.id),
)
.await?;
let mut directory = entry.path().parent().unwrap();
while directory.read_dir().unwrap().next().is_none() {
remove_directory(progress_bar, &directory, false).await?;
if directory == entry.path() {
break;
}
directory = directory.parent().unwrap();
}
}
}
}
}
_ => {
print_warning(progress_bar, "No match");
}
}
commit_transaction(transaction).await;
Ok(system.id)
}
#[allow(clippy::too_many_arguments)]
async fn import_archive(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &Option<&System>,
header: &Option<Header>,
game_ids: &HashSet<i64>,
romfile: CommonRomfile,
romfile_extension: &str,
trash: bool,
delete: bool,
unattended_mode: UnattendedMode,
) -> Result<(HashSet<i64>, HashSet<i64>)> {
let tmp_directory = create_tmp_directory(connection).await?;
let hash_algorithms = HashAlgorithm::iter().collect::<Vec<HashAlgorithm>>();
let archive_romfiles = romfile.as_archive(progress_bar, None).await?;
let romfiles_count = archive_romfiles.len();
let mut roms_games_systems_archive_romfiles: Vec<(
Rom,
Game,
System,
sevenzip::ArchiveRomfile,
)> = vec![];
let mut new_system_ids: HashSet<i64> = HashSet::new();
let mut new_game_ids: HashSet<i64> = HashSet::new();
for archive_romfile in archive_romfiles {
print_action(
progress_bar,
&format!(
"Processing \"{} ({})\"",
archive_romfile.path,
archive_romfile
.romfile
.path
.file_name()
.unwrap()
.to_str()
.unwrap()
),
);
let mut invalid_match_results: Vec<MatchResult> = vec![];
for hash_algorithm in &hash_algorithms {
let (hash, size) = match header {
Some(header) => {
let romfile = archive_romfile
.to_common(progress_bar, &tmp_directory.path())
.await?;
let (hash, size) = romfile
.get_headered_hash_and_size(
connection,
progress_bar,
header,
1,
1,
hash_algorithm,
)
.await?;
romfile.delete(progress_bar, true).await?;
(hash, size)
}
None => {
archive_romfile
.get_hash_and_size(connection, progress_bar, 1, 1, hash_algorithm)
.await?
}
};
let mut game_names: Vec<&str> = vec![];
game_names.push(
archive_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap(),
);
let rom_path = Path::new(&archive_romfile.path);
if let Some(path) = rom_path.parent()
&& let Some(file_name) = path.file_name()
{
let game_name = file_name.to_str().unwrap();
if !game_name.is_empty() {
game_names.push(game_name);
}
}
let rom_name = rom_path.file_name().unwrap().to_str();
let match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
size,
&hash,
system,
game_ids,
game_names.as_slice(),
rom_name,
hash_algorithm,
unattended_mode,
)
.await?;
match match_result.state {
MatchState::Valid => {
let system = match_result.system.unwrap();
let game = match_result.game.unwrap();
let rom = match_result.rom.unwrap();
new_system_ids.insert(system.id);
new_game_ids.insert(game.id);
roms_games_systems_archive_romfiles.push((rom, game, system, archive_romfile));
break;
}
MatchState::Duplicate => {
trash_or_delete_romfile(
connection,
progress_bar,
&romfile,
trash,
delete,
&match_result,
)
.await?;
break;
}
MatchState::Invalid => {
invalid_match_results.push(match_result);
}
MatchState::Skipped => {}
}
}
if invalid_match_results.len() == hash_algorithms.len() && romfiles_count == 1 {
trash_or_delete_romfile(
connection,
progress_bar,
&romfile,
trash,
delete,
invalid_match_results.first().unwrap(),
)
.await?;
}
}
if roms_games_systems_archive_romfiles.len() == romfiles_count && new_game_ids.len() == 1 {
let rom_ids: HashSet<i64> =
find_roms_by_game_id_no_parents(connection, *new_game_ids.iter().last().unwrap())
.await
.into_par_iter()
.map(|rom| rom.id)
.collect();
if rom_ids
.difference(
&roms_games_systems_archive_romfiles
.par_iter()
.map(|(rom, _, _, _)| rom.id)
.collect(),
)
.count()
== 0
{
let game = &roms_games_systems_archive_romfiles.first().unwrap().1;
let system = &roms_games_systems_archive_romfiles.first().unwrap().2;
let system_directory = get_system_directory(connection, system).await?;
for (rom, _game, _system, archive_romfile) in &roms_games_systems_archive_romfiles {
if archive_romfile.path != rom.name {
archive_romfile.rename_file(progress_bar, &rom.name).await?;
}
}
let new_path = match roms_games_systems_archive_romfiles.len() {
1 => {
let rom = &roms_games_systems_archive_romfiles.first().unwrap().0;
let rom_extension = Path::new(&rom.name)
.extension()
.unwrap_or(&OsString::new())
.to_str()
.unwrap()
.to_lowercase();
if system.arcade || PS3_EXTENSIONS.contains(&rom_extension.as_str()) {
system_directory.join(format!("{}.{}", game.name, romfile_extension))
} else {
system_directory
.join(&rom.name)
.with_extension(romfile_extension)
}
}
_ => system_directory.join(format!("{}.{}", game.name, romfile_extension)),
};
romfile.rename(progress_bar, &new_path, false).await?;
create_or_update_romfile(
connection,
progress_bar,
&new_path,
&roms_games_systems_archive_romfiles
.iter()
.map(|(rom, _, _, _)| rom)
.collect::<Vec<&Rom>>(),
Some(system.id),
)
.await?;
return Ok((new_system_ids, new_game_ids));
}
}
for (rom, game, system, archive_romfile) in roms_games_systems_archive_romfiles {
let original_romfile = archive_romfile
.to_common(progress_bar, &tmp_directory.path())
.await?;
let system_directory = get_system_directory(connection, &system).await?;
let new_path;
if system.arcade || game.jbfolder {
let game = find_game_by_id(connection, rom.game_id).await;
new_path = system_directory.join(game.name).join(&rom.name);
} else {
new_path = system_directory.join(&rom.name);
}
copy_file(progress_bar, &original_romfile.path, &new_path, false).await?;
create_or_update_romfile(
connection,
progress_bar,
&new_path,
&[&rom],
Some(system.id),
)
.await?;
}
Ok((new_system_ids, new_game_ids))
}
#[allow(clippy::too_many_arguments)]
async fn import_chd(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &Option<&System>,
game_ids: &HashSet<i64>,
romfile: CommonRomfile,
trash: bool,
delete: bool,
unattended_mode: UnattendedMode,
) -> Result<Option<[i64; 2]>> {
let tmp_directory = create_tmp_directory(connection).await?;
let hash_algorithms = HashAlgorithm::iter().rev().collect::<Vec<HashAlgorithm>>(); let chd_romfile = romfile.as_chd().await?;
match chd_romfile.chd_type {
ChdType::Cd => {
if chd_romfile.track_count > 1
&& chdman::get_version()
.await?
.as_str()
.cmp(chdman::MIN_SPLITBIN_VERSION)
== Ordering::Less
{
print_warning(
progress_bar,
&format!(
"chdman {} or newer required for splitbin support",
chdman::MIN_SPLITBIN_VERSION
),
);
return Ok(None);
}
let cue_bin_romfile = chd_romfile
.to_cue_bin(progress_bar, &tmp_directory.path(), None, &[], true)
.await?;
let mut roms_games_systems: Vec<(Rom, Game, System)> = vec![];
let mut new_game_ids: HashSet<i64> = HashSet::new();
let mut invalid_match_results: Vec<MatchResult> = vec![];
for bin_romfile in &cue_bin_romfile.bin_romfiles {
for hash_algorithm in &hash_algorithms {
let (hash, size) = bin_romfile
.get_hash_and_size(connection, progress_bar, 1, 1, hash_algorithm)
.await?;
let match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
size,
&hash,
system,
if new_game_ids.is_empty() {
game_ids
} else {
&new_game_ids
},
&[chd_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap()],
chd_romfile.romfile.path.file_name().unwrap().to_str(),
hash_algorithm,
unattended_mode,
)
.await?;
match match_result.state {
MatchState::Valid => {
let system = match_result.system.unwrap();
let game = match_result.game.unwrap();
let rom = match_result.rom.unwrap();
new_game_ids.insert(game.id);
roms_games_systems.push((rom, game, system));
break;
}
MatchState::Duplicate => {
break;
}
MatchState::Invalid => {
invalid_match_results.push(match_result);
}
MatchState::Skipped => {}
}
}
}
let roms = roms_games_systems
.iter()
.map(|rom_game_system| &rom_game_system.0)
.collect::<Vec<&Rom>>();
let mut games = roms_games_systems
.iter()
.map(|rom_game_system| &rom_game_system.1)
.collect::<Vec<&Game>>();
games.dedup_by_key(|game| game.id);
let mut systems = roms_games_systems
.iter()
.map(|rom_game_system| &rom_game_system.2)
.collect::<Vec<&System>>();
systems.dedup_by_key(|system| system.id);
if roms.len() == cue_bin_romfile.bin_romfiles.len()
&& games.len() == 1
&& systems.len() == 1
{
let system = systems.first().unwrap();
let system_directory = get_system_directory(connection, system).await?;
let game = games.first().unwrap();
let new_chd_path =
system_directory.join(format!("{}.{}", game.name, CHD_EXTENSION));
chd_romfile
.romfile
.rename(progress_bar, &new_chd_path, false)
.await?;
create_or_update_romfile(
connection,
progress_bar,
&new_chd_path,
&roms,
Some(system.id),
)
.await?;
return Ok(Some([system.id, game.id]));
}
if !invalid_match_results.is_empty() {
print_error(progress_bar, "CRC mismatch");
trash_or_delete_romfile(
connection,
progress_bar,
&chd_romfile.romfile,
trash,
delete,
&MatchResult {
state: MatchState::Invalid,
system: None,
game: None,
rom: None,
hash_algorithm: None,
hash: None,
},
)
.await?;
}
Ok(None)
}
ChdType::Dvd => {
let mut invalid_match_results: Vec<MatchResult> = vec![];
for hash_algorithm in &hash_algorithms {
let (hash, size) = chd_romfile
.get_hash_and_size(connection, progress_bar, 1, 1, hash_algorithm)
.await?;
let mut match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
size,
&hash,
system,
game_ids,
&[chd_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap()],
chd_romfile.romfile.path.file_name().unwrap().to_str(),
hash_algorithm,
unattended_mode,
)
.await?;
if match_result.state == MatchState::Invalid
&& hash_algorithm == &HashAlgorithm::Sha1
{
match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
0,
&chd_romfile.chd_sha1,
system,
game_ids,
&[chd_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap()],
chd_romfile.romfile.path.file_name().unwrap().to_str(),
hash_algorithm,
unattended_mode,
)
.await?;
}
match match_result.state {
MatchState::Valid => {
let system = match_result.system.unwrap();
let game = match_result.game.unwrap();
let rom = match_result.rom.unwrap();
let system_directory = get_system_directory(connection, &system).await?;
let new_chd_path = system_directory
.join(&rom.name)
.with_extension(CHD_EXTENSION);
chd_romfile
.romfile
.rename(progress_bar, &new_chd_path, false)
.await?;
create_or_update_romfile(
connection,
progress_bar,
&new_chd_path,
&[&rom],
Some(system.id),
)
.await?;
return Ok(Some([system.id, game.id]));
}
MatchState::Duplicate => {
trash_or_delete_romfile(
connection,
progress_bar,
&chd_romfile.romfile,
trash,
delete,
&match_result,
)
.await?;
return Ok(None);
}
MatchState::Invalid => {
invalid_match_results.push(match_result);
}
MatchState::Skipped => {}
}
}
if !invalid_match_results.is_empty() {
print_error(progress_bar, "CRC mismatch");
trash_or_delete_romfile(
connection,
progress_bar,
&chd_romfile.romfile,
trash,
delete,
invalid_match_results.first().unwrap(),
)
.await?;
}
Ok(None)
}
ChdType::Hd => {
let mut invalid_match_results: Vec<MatchResult> = vec![];
for hash_algorithm in &hash_algorithms {
let (hash, size) = chd_romfile
.get_hash_and_size(connection, progress_bar, 1, 1, hash_algorithm)
.await?;
let mut match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
size,
&hash,
system,
game_ids,
&[chd_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap()],
chd_romfile.romfile.path.file_name().unwrap().to_str(),
hash_algorithm,
unattended_mode,
)
.await?;
if match_result.state == MatchState::Invalid
&& hash_algorithm == &HashAlgorithm::Sha1
{
match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
0,
&chd_romfile.chd_sha1,
system,
game_ids,
&[chd_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap()],
chd_romfile.romfile.path.file_name().unwrap().to_str(),
hash_algorithm,
unattended_mode,
)
.await?;
}
match match_result.state {
MatchState::Valid => {
let system = match_result.system.unwrap();
let game = match_result.game.unwrap();
let rom = match_result.rom.unwrap();
let system_directory = get_system_directory(connection, &system).await?;
let new_chd_path = if system.arcade {
let game = find_game_by_id(connection, rom.game_id).await;
system_directory
.join(game.name)
.join(&rom.name)
.with_extension(CHD_EXTENSION)
} else {
system_directory
.join(&rom.name)
.with_extension(CHD_EXTENSION)
};
chd_romfile
.romfile
.rename(progress_bar, &new_chd_path, false)
.await?;
create_or_update_romfile(
connection,
progress_bar,
&new_chd_path,
&[&rom],
Some(system.id),
)
.await?;
return Ok(Some([system.id, game.id]));
}
MatchState::Duplicate => {
trash_or_delete_romfile(
connection,
progress_bar,
&chd_romfile.romfile,
trash,
delete,
&match_result,
)
.await?;
return Ok(None);
}
MatchState::Invalid => {
invalid_match_results.push(match_result);
}
MatchState::Skipped => {}
}
}
if !invalid_match_results.is_empty() {
print_error(progress_bar, "CRC mismatch");
trash_or_delete_romfile(
connection,
progress_bar,
&chd_romfile.romfile,
trash,
delete,
invalid_match_results.first().unwrap(),
)
.await?;
}
Ok(None)
}
ChdType::Ld => {
let mut invalid_match_results: Vec<MatchResult> = vec![];
for hash_algorithm in &hash_algorithms {
let (hash, size) = chd_romfile
.get_hash_and_size(connection, progress_bar, 1, 1, hash_algorithm)
.await?;
let mut match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
size,
&hash,
system,
game_ids,
&[chd_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap()],
chd_romfile.romfile.path.file_name().unwrap().to_str(),
hash_algorithm,
unattended_mode,
)
.await?;
if match_result.state == MatchState::Invalid
&& hash_algorithm == &HashAlgorithm::Sha1
{
match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
0,
&chd_romfile.chd_sha1,
system,
game_ids,
&[chd_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap()],
chd_romfile.romfile.path.file_name().unwrap().to_str(),
hash_algorithm,
unattended_mode,
)
.await?;
}
match match_result.state {
MatchState::Valid => {
let system = match_result.system.unwrap();
let game = match_result.game.unwrap();
let rom = match_result.rom.unwrap();
let system_directory = get_system_directory(connection, &system).await?;
let new_chd_path = if system.arcade {
let game = find_game_by_id(connection, rom.game_id).await;
system_directory
.join(game.name)
.join(&rom.name)
.with_extension(CHD_EXTENSION)
} else {
system_directory
.join(&rom.name)
.with_extension(CHD_EXTENSION)
};
chd_romfile
.romfile
.rename(progress_bar, &new_chd_path, false)
.await?;
create_or_update_romfile(
connection,
progress_bar,
&new_chd_path,
&[&rom],
Some(system.id),
)
.await?;
return Ok(Some([system.id, game.id]));
}
MatchState::Duplicate => {
trash_or_delete_romfile(
connection,
progress_bar,
&chd_romfile.romfile,
trash,
delete,
&match_result,
)
.await?;
return Ok(None);
}
MatchState::Invalid => {
invalid_match_results.push(match_result);
}
MatchState::Skipped => {}
}
}
if !invalid_match_results.is_empty() {
print_error(progress_bar, "CRC mismatch");
trash_or_delete_romfile(
connection,
progress_bar,
&chd_romfile.romfile,
trash,
delete,
invalid_match_results.first().unwrap(),
)
.await?;
}
Ok(None)
}
}
}
#[allow(clippy::too_many_arguments)]
async fn import_cia(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &Option<&System>,
game_ids: &HashSet<i64>,
romfile: CommonRomfile,
trash: bool,
delete: bool,
unattended_mode: UnattendedMode,
) -> Result<(HashSet<i64>, HashSet<i64>)> {
let tmp_directory = create_tmp_directory(connection).await?;
let cia_infos = ctrtool::parse_cia(progress_bar, &romfile.path).await?;
let mut match_results_cia_infos: Vec<(MatchResult, &ctrtool::ArchiveInfo)> = vec![];
let mut new_system_ids: HashSet<i64> = HashSet::new();
let mut new_game_ids: HashSet<i64> = HashSet::new();
let extracted_files =
ctrtool::extract_files_from_cia(progress_bar, &romfile.path, &tmp_directory.path()).await?;
let mut invalid_match_results: Vec<MatchResult> = vec![];
for (cia_info, extracted_path) in cia_infos.iter().zip(extracted_files) {
print_action(
progress_bar,
&format!(
"Processing \"{} ({})\"",
cia_info.path,
romfile.path.file_name().unwrap().to_str().unwrap()
),
);
let extracted_romfile = CommonRomfile::from_path(&extracted_path)?;
for hash_algorithm in HashAlgorithm::iter() {
let (hash, size) = extracted_romfile
.get_hash_and_size(connection, progress_bar, 1, 1, &hash_algorithm)
.await?;
let path = Path::new(&cia_info.path);
let mut game_names: Vec<&str> = vec![];
game_names.push(romfile.path.file_stem().unwrap().to_str().unwrap());
if let Some(path) = path.parent() {
let game_name = path.as_os_str().to_str().unwrap();
if !game_name.is_empty() {
game_names.push(game_name);
}
}
let rom_name = path.file_name().unwrap().to_str();
let match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
size,
&hash,
system,
game_ids,
game_names.as_slice(),
rom_name,
&hash_algorithm,
unattended_mode,
)
.await?;
if match_result.state == MatchState::Valid {
new_system_ids.insert(match_result.system.as_ref().unwrap().id);
new_game_ids.insert(match_result.game.as_ref().unwrap().id);
match_results_cia_infos.push((match_result, cia_info));
break;
} else if match_result.state == MatchState::Invalid {
invalid_match_results.push(match_result);
}
}
extracted_romfile.delete(progress_bar, true).await?;
}
if match_results_cia_infos.len() == cia_infos.len() && new_game_ids.len() == 1 {
let rom_ids: HashSet<i64> =
find_roms_by_game_id_no_parents(connection, *new_game_ids.iter().last().unwrap())
.await
.into_par_iter()
.map(|rom| rom.id)
.collect();
if rom_ids.is_superset(
&match_results_cia_infos
.par_iter()
.map(|(match_result, _)| match_result.rom.as_ref().unwrap().id)
.collect(),
) {
let first_match = &match_results_cia_infos.first().unwrap().0;
let system = first_match.system.as_ref().unwrap();
let game = first_match.game.as_ref().unwrap();
let roms = match_results_cia_infos
.iter()
.map(|(match_result, _)| match_result.rom.as_ref().unwrap())
.collect::<Vec<&Rom>>();
let system_directory = get_system_directory(connection, system).await?;
let new_path = system_directory.join(format!("{}.cia", game.name));
romfile.rename(progress_bar, &new_path, false).await?;
create_or_update_romfile(connection, progress_bar, &new_path, &roms, Some(system.id))
.await?;
return Ok((new_system_ids, new_game_ids));
}
}
if match_results_cia_infos
.iter()
.all(|(match_result, _)| match_result.state == MatchState::Duplicate)
{
trash_or_delete_romfile(
connection,
progress_bar,
&romfile,
trash,
delete,
&match_results_cia_infos.first().unwrap().0,
)
.await?;
} else if !invalid_match_results.is_empty() {
trash_or_delete_romfile(
connection,
progress_bar,
&romfile,
trash,
delete,
&MatchResult {
state: MatchState::Invalid,
system: None,
game: None,
rom: None,
hash_algorithm: None,
hash: None,
},
)
.await?;
}
Ok((new_system_ids, new_game_ids))
}
#[allow(clippy::too_many_arguments)]
async fn import_cso(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &Option<&System>,
game_ids: &HashSet<i64>,
romfile: CommonRomfile,
trash: bool,
delete: bool,
unattended_mode: UnattendedMode,
) -> Result<Option<[i64; 2]>> {
let cso_romfile = romfile.as_xso().await?;
let mut invalid_match_results: Vec<MatchResult> = vec![];
for hash_algorithm in HashAlgorithm::iter() {
let (hash, size) = cso_romfile
.get_hash_and_size(connection, progress_bar, 1, 1, &hash_algorithm)
.await?;
let match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
size,
&hash,
system,
game_ids,
&[cso_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap()],
cso_romfile.romfile.path.file_name().unwrap().to_str(),
&hash_algorithm,
unattended_mode,
)
.await?;
match match_result.state {
MatchState::Valid => {
let system = match_result.system.unwrap();
let game = match_result.game.unwrap();
let rom = match_result.rom.unwrap();
let system_directory = get_system_directory(connection, &system).await?;
let new_path = system_directory
.join(&rom.name)
.with_extension(CSO_EXTENSION);
cso_romfile
.romfile
.rename(progress_bar, &new_path, false)
.await?;
create_or_update_romfile(
connection,
progress_bar,
&new_path,
&[&rom],
Some(system.id),
)
.await?;
return Ok(Some([system.id, game.id]));
}
MatchState::Duplicate => {
trash_or_delete_romfile(
connection,
progress_bar,
&cso_romfile.romfile,
trash,
delete,
&match_result,
)
.await?;
return Ok(None);
}
MatchState::Invalid => {
invalid_match_results.push(match_result);
}
MatchState::Skipped => {}
}
}
if !invalid_match_results.is_empty() {
trash_or_delete_romfile(
connection,
progress_bar,
&cso_romfile.romfile,
trash,
delete,
invalid_match_results.first().unwrap(),
)
.await?;
}
Ok(None)
}
#[allow(clippy::too_many_arguments)]
async fn import_nsz(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &Option<&System>,
game_ids: &HashSet<i64>,
romfile: CommonRomfile,
trash: bool,
delete: bool,
unattended_mode: UnattendedMode,
) -> Result<Option<[i64; 2]>> {
let nsz_romfile = romfile.as_nsz()?;
let mut invalid_match_results: Vec<MatchResult> = vec![];
for hash_algorithm in HashAlgorithm::iter() {
let (hash, size) = nsz_romfile
.get_hash_and_size(connection, progress_bar, 1, 1, &hash_algorithm)
.await?;
let match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
size,
&hash,
system,
game_ids,
&[nsz_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap()],
nsz_romfile.romfile.path.file_name().unwrap().to_str(),
&hash_algorithm,
unattended_mode,
)
.await?;
match match_result.state {
MatchState::Valid => {
let system = match_result.system.unwrap();
let game = match_result.game.unwrap();
let rom = match_result.rom.unwrap();
let system_directory = get_system_directory(connection, &system).await?;
let new_nsz_path = system_directory
.join(&rom.name)
.with_extension(NSZ_EXTENSION);
nsz_romfile
.romfile
.rename(progress_bar, &new_nsz_path, false)
.await?;
create_or_update_romfile(
connection,
progress_bar,
&new_nsz_path,
&[&rom],
Some(system.id),
)
.await?;
return Ok(Some([system.id, game.id]));
}
MatchState::Duplicate => {
trash_or_delete_romfile(
connection,
progress_bar,
&nsz_romfile.romfile,
trash,
delete,
&match_result,
)
.await?;
return Ok(None);
}
MatchState::Invalid => {
invalid_match_results.push(match_result);
}
MatchState::Skipped => {}
}
}
if !invalid_match_results.is_empty() {
trash_or_delete_romfile(
connection,
progress_bar,
&nsz_romfile.romfile,
trash,
delete,
invalid_match_results.first().unwrap(),
)
.await?;
}
Ok(None)
}
#[allow(clippy::too_many_arguments)]
async fn import_rvz(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &Option<&System>,
game_ids: &HashSet<i64>,
romfile: CommonRomfile,
trash: bool,
delete: bool,
unattended_mode: UnattendedMode,
) -> Result<Option<[i64; 2]>> {
let rvz_romfile = romfile.as_rvz()?;
let mut invalid_match_results: Vec<MatchResult> = vec![];
for hash_algorithm in HashAlgorithm::iter() {
let (hash, size) = rvz_romfile
.get_hash_and_size(connection, progress_bar, 1, 1, &hash_algorithm)
.await?;
let match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
size,
&hash,
system,
game_ids,
&[rvz_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap()],
rvz_romfile.romfile.path.file_name().unwrap().to_str(),
&hash_algorithm,
unattended_mode,
)
.await?;
match match_result.state {
MatchState::Valid => {
let system = match_result.system.unwrap();
let game = match_result.game.unwrap();
let rom = match_result.rom.unwrap();
let system_directory = get_system_directory(connection, &system).await?;
let new_rvz_path = system_directory
.join(&rom.name)
.with_extension(RVZ_EXTENSION);
rvz_romfile
.romfile
.rename(progress_bar, &new_rvz_path, false)
.await?;
create_or_update_romfile(
connection,
progress_bar,
&new_rvz_path,
&[&rom],
Some(system.id),
)
.await?;
return Ok(Some([system.id, game.id]));
}
MatchState::Duplicate => {
trash_or_delete_romfile(
connection,
progress_bar,
&rvz_romfile.romfile,
trash,
delete,
&match_result,
)
.await?;
return Ok(None);
}
MatchState::Invalid => {
invalid_match_results.push(match_result);
}
MatchState::Skipped => {}
}
}
if !invalid_match_results.is_empty() {
trash_or_delete_romfile(
connection,
progress_bar,
&rvz_romfile.romfile,
trash,
delete,
invalid_match_results.first().unwrap(),
)
.await?;
}
Ok(None)
}
#[allow(clippy::too_many_arguments)]
async fn import_zso(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &Option<&System>,
game_ids: &HashSet<i64>,
romfile: CommonRomfile,
trash: bool,
delete: bool,
unattended_mode: UnattendedMode,
) -> Result<Option<[i64; 2]>> {
let zso_romfile = romfile.as_xso().await?;
let mut invalid_match_results: Vec<MatchResult> = vec![];
for hash_algorithm in HashAlgorithm::iter() {
let (hash, size) = zso_romfile
.get_hash_and_size(connection, progress_bar, 1, 1, &hash_algorithm)
.await?;
let match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
size,
&hash,
system,
game_ids,
&[zso_romfile
.romfile
.path
.file_stem()
.unwrap()
.to_str()
.unwrap()],
zso_romfile.romfile.path.file_name().unwrap().to_str(),
&hash_algorithm,
unattended_mode,
)
.await?;
match match_result.state {
MatchState::Valid => {
let system = match_result.system.unwrap();
let game = match_result.game.unwrap();
let rom = match_result.rom.unwrap();
let system_directory = get_system_directory(connection, &system).await?;
let new_zso_path = system_directory
.join(&rom.name)
.with_extension(ZSO_EXTENSION);
zso_romfile
.romfile
.rename(progress_bar, &new_zso_path, false)
.await?;
create_or_update_romfile(
connection,
progress_bar,
&new_zso_path,
&[&rom],
Some(system.id),
)
.await?;
return Ok(Some([system.id, game.id]));
}
MatchState::Duplicate => {
trash_or_delete_romfile(
connection,
progress_bar,
&zso_romfile.romfile,
trash,
delete,
&match_result,
)
.await?;
return Ok(None);
}
MatchState::Invalid => {
invalid_match_results.push(match_result);
}
MatchState::Skipped => {}
}
}
if !invalid_match_results.is_empty() {
trash_or_delete_romfile(
connection,
progress_bar,
&zso_romfile.romfile,
trash,
delete,
invalid_match_results.first().unwrap(),
)
.await?;
}
Ok(None)
}
#[allow(clippy::too_many_arguments)]
pub async fn import_other(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &Option<&System>,
header: &Option<Header>,
game_ids: &HashSet<i64>,
romfile: CommonRomfile,
trash: bool,
delete: bool,
unattended_mode: UnattendedMode,
) -> Result<Option<[i64; 2]>> {
let hash_algorithms = HashAlgorithm::iter().collect::<Vec<HashAlgorithm>>();
let mut invalid_match_results: Vec<MatchResult> = vec![];
for hash_algorithm in &hash_algorithms {
let (hash, size) = match header {
Some(header) => {
romfile
.get_headered_hash_and_size(
connection,
progress_bar,
header,
1,
1,
hash_algorithm,
)
.await?
}
None => {
romfile
.get_hash_and_size(connection, progress_bar, 1, 1, hash_algorithm)
.await?
}
};
let match_result = find_rom_by_size_and_hash(
connection,
progress_bar,
size,
&hash,
system,
game_ids,
&[romfile.path.file_stem().unwrap().to_str().unwrap()],
romfile.path.file_name().unwrap().to_str(),
hash_algorithm,
unattended_mode,
)
.await?;
match match_result.state {
MatchState::Valid => {
let system = match_result.system.unwrap();
let game = match_result.game.unwrap();
let rom = match_result.rom.unwrap();
let new_path = romfile
.get_sorted_path(connection, &system, &game, &rom, &None, &None)
.await?;
romfile.rename(progress_bar, &new_path, false).await?;
create_or_update_romfile(
connection,
progress_bar,
&new_path,
&[&rom],
Some(system.id),
)
.await?;
return Ok(Some([system.id, game.id]));
}
MatchState::Duplicate => {
trash_or_delete_romfile(
connection,
progress_bar,
&romfile,
trash,
delete,
&match_result,
)
.await?;
return Ok(None);
}
MatchState::Invalid => {
invalid_match_results.push(match_result);
}
MatchState::Skipped => {}
}
}
if !invalid_match_results.is_empty() {
trash_or_delete_romfile(
connection,
progress_bar,
&romfile,
trash,
delete,
invalid_match_results.first().unwrap(),
)
.await?;
}
Ok(None)
}
#[allow(clippy::too_many_arguments)]
async fn find_rom_by_size_and_hash(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
size: u64,
hash: &str,
system: &Option<&System>,
game_ids: &HashSet<i64>,
game_names: &[&str],
rom_name: Option<&str>,
hash_algorithm: &HashAlgorithm,
unattended_mode: UnattendedMode,
) -> Result<MatchResult> {
let mut roms: Vec<Rom> = vec![];
if !game_names.is_empty()
&& let Some(rom_name) = rom_name
{
match hash_algorithm {
HashAlgorithm::Crc => {
if let Some(system) = system {
find_roms_without_romfile_by_name_and_size_and_crc_and_game_names_and_system_id(
connection, rom_name, size, hash, game_names, system.id,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
} else {
find_roms_without_romfile_by_name_and_size_and_crc_and_game_names(
connection, rom_name, size, hash, game_names,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
}
}
HashAlgorithm::Md5 => {
if let Some(system) = system {
find_roms_without_romfile_by_name_and_size_and_md5_and_game_names_and_system_id(
connection, rom_name, size, hash, game_names, system.id,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
} else {
find_roms_without_romfile_by_name_and_size_and_md5_and_game_names(
connection, rom_name, size, hash, game_names,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
}
}
HashAlgorithm::Sha1 => {
if let Some(system) = system {
find_roms_without_romfile_by_name_and_size_and_sha1_and_game_names_and_system_id(
connection,
rom_name,
size,
hash,
game_names,
system.id,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
} else {
find_roms_without_romfile_by_name_and_size_and_sha1_and_game_names(
connection, rom_name, size, hash, game_names,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
}
}
};
}
if roms.is_empty() && !game_names.is_empty() {
match hash_algorithm {
HashAlgorithm::Crc => {
if let Some(system) = system {
find_roms_without_romfile_by_size_and_crc_and_game_names_and_system_id(
connection, size, hash, game_names, system.id,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
} else {
find_roms_without_romfile_by_size_and_crc_and_game_names(
connection, size, hash, game_names,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
}
}
HashAlgorithm::Md5 => {
if let Some(system) = system {
find_roms_without_romfile_by_size_and_md5_and_game_names_and_system_id(
connection, size, hash, game_names, system.id,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
} else {
find_roms_without_romfile_by_size_and_md5_and_game_names(
connection, size, hash, game_names,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
}
}
HashAlgorithm::Sha1 => {
if let Some(system) = system {
find_roms_without_romfile_by_size_and_sha1_and_game_names_and_system_id(
connection, size, hash, game_names, system.id,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
} else {
find_roms_without_romfile_by_size_and_sha1_and_game_names(
connection, size, hash, game_names,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
}
}
};
}
if roms.is_empty() {
match hash_algorithm {
HashAlgorithm::Crc => {
if let Some(system) = system {
find_roms_without_romfile_by_size_and_crc_and_system_id(
connection, size, hash, system.id,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
} else {
find_roms_without_romfile_by_size_and_crc(connection, size, hash)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
}
}
HashAlgorithm::Md5 => {
if let Some(system) = system {
find_roms_without_romfile_by_size_and_md5_and_system_id(
connection, size, hash, system.id,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
} else {
find_roms_without_romfile_by_size_and_md5(connection, size, hash)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
}
}
HashAlgorithm::Sha1 => {
if let Some(system) = system {
find_roms_without_romfile_by_size_and_sha1_and_system_id(
connection, size, hash, system.id,
)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
} else {
find_roms_without_romfile_by_size_and_sha1(connection, size, hash)
.await
.into_iter()
.for_each(|rom| roms.push(rom))
}
}
};
}
if roms.is_empty() {
let rom_count = match hash_algorithm {
HashAlgorithm::Crc => match system {
Some(system) => {
count_roms_with_romfile_by_size_and_crc_and_system_id(
connection, size, hash, system.id,
)
.await
}
None => count_roms_with_romfile_by_size_and_crc(connection, size, hash).await,
},
HashAlgorithm::Md5 => match system {
Some(system) => {
count_roms_with_romfile_by_size_and_md5_and_system_id(
connection, size, hash, system.id,
)
.await
}
None => count_roms_with_romfile_by_size_and_md5(connection, size, hash).await,
},
HashAlgorithm::Sha1 => match system {
Some(system) => {
count_roms_with_romfile_by_size_and_sha1_and_system_id(
connection, size, hash, system.id,
)
.await
}
None => count_roms_with_romfile_by_size_and_sha1(connection, size, hash).await,
},
};
if rom_count > 0 {
print_skip(progress_bar, "Already imported");
return Ok(MatchResult {
state: MatchState::Duplicate,
system: None,
game: None,
rom: None,
hash_algorithm: Some(hash_algorithm.to_string().to_lowercase()),
hash: Some(hash.to_string()),
});
} else {
print_warning(progress_bar, "No match");
return Ok(MatchResult {
state: MatchState::Invalid,
system: None,
game: None,
rom: None,
hash_algorithm: Some(hash_algorithm.to_string().to_lowercase()),
hash: Some(hash.to_string()),
});
}
}
let mut rom_game_system: Option<(Rom, Game, System)> = None;
if roms.len() == 1 {
let rom = roms.remove(0);
let game = find_game_by_id(connection, rom.game_id).await;
let system = find_system_by_id(connection, game.system_id).await;
print_success(progress_bar, &format!("Matches \"{}\"", rom.name));
rom_game_system = Some((rom, game, system));
} else if roms.iter().any(|rom| game_ids.contains(&rom.game_id)) {
let rom = roms
.into_iter()
.find(|rom| game_ids.contains(&rom.game_id))
.unwrap();
let game = find_game_by_id(connection, rom.game_id).await;
let system = find_system_by_id(connection, game.system_id).await;
print_success(progress_bar, &format!("Matches \"{}\"", rom.name));
rom_game_system = Some((rom, game, system));
} else if unattended_mode == UnattendedMode::First
&& let Some(rom_name) = rom_name
{
roms.sort_by(|a, b| {
let a_distance = jaro_winkler(&rom_name.to_lowercase(), &a.name.to_lowercase());
let b_distance = jaro_winkler(&rom_name.to_lowercase(), &b.name.to_lowercase());
a_distance.partial_cmp(&b_distance).unwrap()
});
let rom = roms.remove(0);
let game = find_game_by_id(connection, rom.game_id).await;
let system = find_system_by_id(connection, game.system_id).await;
print_success(progress_bar, &format!("Matches \"{}\"", rom.name));
rom_game_system = Some((rom, game, system));
} else if system.is_some() {
let mut roms_games: Vec<(Rom, Game)> = vec![];
for rom in roms {
let game = find_game_by_id(connection, rom.game_id).await;
roms_games.push((rom, game));
}
if let Some((rom, game)) = prompt_for_rom_game(&mut roms_games)? {
let system = find_system_by_id(connection, game.system_id).await;
rom_game_system = Some((rom, game, system));
};
} else if unattended_mode == UnattendedMode::Skip {
print_warning(progress_bar, "Multiple matches, skipping");
return Ok(MatchResult {
state: MatchState::Skipped,
system: None,
game: None,
rom: None,
hash_algorithm: None,
hash: None,
});
} else {
let mut roms_games_systems: Vec<(Rom, Game, System)> = vec![];
for rom in roms {
let game = find_game_by_id(connection, rom.game_id).await;
let system = find_system_by_id(connection, game.system_id).await;
roms_games_systems.push((rom, game, system));
}
rom_game_system = prompt_for_rom_game_system(&mut roms_games_systems)?;
}
if let Some((rom, _, _)) = &rom_game_system
&& let Some(romfile_id) = rom.romfile_id
{
let romfile = find_romfile_by_id(connection, romfile_id).await;
print_skip(progress_bar, &format!("Duplicate of \"{}\"", romfile.path));
let (rom, game, system) = rom_game_system.unwrap();
return Ok(MatchResult {
state: MatchState::Duplicate,
system: Some(system),
game: Some(game),
rom: Some(rom),
hash_algorithm: Some(hash_algorithm.to_string().to_lowercase()),
hash: Some(hash.to_string()),
});
}
let (rom, game, system) = rom_game_system.unwrap();
Ok(MatchResult {
state: MatchState::Valid,
system: Some(system),
game: Some(game),
rom: Some(rom),
hash_algorithm: Some(hash_algorithm.to_string().to_lowercase()),
hash: Some(hash.to_string()),
})
}
async fn find_sfb_rom_by_md5(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &System,
size: u64,
md5: &str,
unattended_mode: UnattendedMode,
) -> Result<MatchResult> {
let mut roms = find_sfb_roms_without_romfile_by_name_and_size_and_md5_and_system_id(
connection,
PS3_DISC_SFB,
size,
md5,
system.id,
)
.await;
if roms.is_empty() {
if count_sfb_roms_with_romfile_by_name_and_size_and_md5_and_system_id(
connection,
PS3_DISC_SFB,
size,
md5,
system.id,
)
.await
> 0
{
print_skip(progress_bar, "Already imported");
return Ok(MatchResult {
state: MatchState::Duplicate,
system: None,
game: None,
rom: None,
hash_algorithm: Some(HashAlgorithm::Md5.to_string().to_lowercase()),
hash: Some(md5.to_string()),
});
} else {
print_warning(progress_bar, "No match");
return Ok(MatchResult {
state: MatchState::Invalid,
system: None,
game: None,
rom: None,
hash_algorithm: Some(HashAlgorithm::Md5.to_string().to_lowercase()),
hash: Some(md5.to_string()),
});
}
}
let rom_game: Option<(Rom, Game)>;
if roms.len() == 1 || unattended_mode == UnattendedMode::First {
let rom = roms.remove(0);
let game = find_game_by_id(connection, rom.game_id).await;
print_success(progress_bar, &format!("Matches \"{}\"", rom.name));
rom_game = Some((rom, game));
} else if unattended_mode == UnattendedMode::Skip {
print_warning(progress_bar, "Multiple matches, skipping");
return Ok(MatchResult {
state: MatchState::Skipped,
system: None,
game: None,
rom: None,
hash_algorithm: None,
hash: None,
});
} else {
let mut roms_games: Vec<(Rom, Game)> = vec![];
for rom in roms {
let game = find_game_by_id(connection, rom.game_id).await;
roms_games.push((rom, game));
}
rom_game = prompt_for_rom_game(&mut roms_games)?;
}
if let Some((rom, _)) = &rom_game
&& let Some(romfile_id) = rom.romfile_id
{
let romfile = find_romfile_by_id(connection, romfile_id).await;
print_skip(progress_bar, &format!("Duplicate of \"{}\"", romfile.path));
let (rom, game) = rom_game.unwrap();
return Ok(MatchResult {
state: MatchState::Duplicate,
system: None,
game: Some(game),
rom: Some(rom),
hash_algorithm: Some(HashAlgorithm::Md5.to_string().to_lowercase()),
hash: Some(md5.to_string()),
});
}
let (rom, game) = rom_game.unwrap();
Ok(MatchResult {
state: MatchState::Valid,
system: None,
game: Some(game),
rom: Some(rom),
hash_algorithm: Some(HashAlgorithm::Md5.to_string().to_lowercase()),
hash: Some(md5.to_string()),
})
}
async fn create_or_update_romfile<P: AsRef<Path>>(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
romfile_path: &P,
roms: &[&Rom],
system_id: Option<i64>,
) -> Result<()> {
let romfile = CommonRomfile::from_path(&romfile_path)?.with_system(system_id);
let relative_path = romfile.get_relative_path(connection).await?;
let existing_romfile =
find_romfile_by_path(connection, relative_path.as_os_str().to_str().unwrap()).await;
let romfile_id = match existing_romfile {
Some(existing_romfile) => {
romfile
.update(connection, progress_bar, existing_romfile.id)
.await?;
existing_romfile.id
}
None => {
romfile
.create(connection, progress_bar, RomfileType::Romfile)
.await?
}
};
for rom in roms {
update_rom_romfile(connection, rom.id, Some(romfile_id)).await;
}
Ok(())
}
async fn trash_or_delete_romfile(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
romfile: &CommonRomfile,
trash: bool,
delete: bool,
match_result: &MatchResult,
) -> Result<()> {
if delete {
romfile.delete(progress_bar, false).await?;
} else if trash {
let original_filename = romfile.path.file_name().unwrap().to_str().unwrap();
let new_filename = if let (Some(hash_algorithm), Some(hash)) =
(&match_result.hash_algorithm, &match_result.hash)
{
let stem = romfile
.path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(original_filename);
let extension = romfile.path.extension().and_then(|e| e.to_str());
if let Some(ext) = extension {
format!("{} [{}:{}].{}", stem, hash_algorithm, hash, ext)
} else {
format!("{} [{}:{}]", stem, hash_algorithm, hash)
}
} else {
original_filename.to_string()
};
let new_path = get_trash_directory(connection, match_result.system.as_ref())
.await?
.join(match_result.state.to_string())
.join(new_filename);
if new_path.exists() {
romfile.delete(progress_bar, false).await?;
} else {
let system_id = match_result.system.as_ref().map(|s| s.id);
let new_romfile = romfile
.rename(progress_bar, &new_path, false)
.await?
.with_system(system_id);
match find_romfile_by_path(connection, new_path.as_os_str().to_str().unwrap()).await {
Some(romfile) => {
new_romfile
.update(connection, progress_bar, romfile.id)
.await?;
}
None => {
new_romfile
.create(connection, progress_bar, RomfileType::Romfile)
.await?;
}
}
}
}
Ok(())
}
#[cfg(test)]
mod test_cia;
#[cfg(test)]
mod test_cso;
#[cfg(test)]
mod test_iso_chd;
#[cfg(test)]
mod test_mame;
#[cfg(test)]
mod test_mame_chd;
#[cfg(test)]
mod test_mame_mixed;
#[cfg(test)]
mod test_multiple_tracks_chd;
#[cfg(test)]
mod test_original;
#[cfg(test)]
mod test_original_delete;
#[cfg(test)]
mod test_original_headered;
#[cfg(test)]
mod test_original_shared;
#[cfg(test)]
mod test_original_trash;
#[cfg(test)]
mod test_per_system_rom_directory;
#[cfg(test)]
mod test_rvz;
#[cfg(test)]
mod test_rvz_as_is;
#[cfg(test)]
mod test_sevenzip_multiple_files_full_game;
#[cfg(test)]
mod test_sevenzip_multiple_files_headered_mixed_games;
#[cfg(test)]
mod test_sevenzip_multiple_files_mixed_games;
#[cfg(test)]
mod test_sevenzip_multiple_files_partial_game;
#[cfg(test)]
mod test_sevenzip_single_file;
#[cfg(test)]
mod test_sevenzip_single_file_headered;
#[cfg(test)]
mod test_single_track_chd;
#[cfg(test)]
mod test_zip_single_file;
#[cfg(test)]
mod test_zip_single_file_extract;
#[cfg(test)]
mod test_zip_special_characters;
#[cfg(test)]
mod test_zso;