diff --git a/Cargo.lock b/Cargo.lock index 045a320..1c80b3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,7 +264,7 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "xbst" -version = "0.1.2" +version = "0.2.0" dependencies = [ "clap", "deunicode", diff --git a/Cargo.toml b/Cargo.toml index 0016c7a..d6994d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xbst" -version = "0.1.2" +version = "0.2.0" edition = "2021" [dependencies] diff --git a/readme.md b/readme.md index 455a544..c61f4dd 100644 --- a/readme.md +++ b/readme.md @@ -50,4 +50,4 @@ Options: - Some files, once converted, are quieter than usual? - When using wmav1, some audio files might sounds absolute ass. - Untested with a large library, probably has issues? -- Code is poo poo :( \ No newline at end of file +- Code is still poo poo but a bit better \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index f26edf8..d191a76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ use std::{ ffi::OsStr, - fs::{self, read_dir, File}, + fs::{self, read_dir, DirEntry, File}, io::{stdout, Write}, - path::PathBuf, + path::{Path, PathBuf}, process::Command, string::FromUtf8Error, }; @@ -14,7 +14,7 @@ use deunicode::AsciiChars; use thiserror::Error; use zerocopy::IntoBytes; -use crate::utils::{Codec, Header, MusicFile, Song, Soundtrack}; +use crate::utils::{Ansi, Codec, Header, Song, SongGroup, Soundtrack}; #[derive(Error, Debug)] enum Errors { @@ -79,21 +79,16 @@ fn main() { } fn process(args: &Args) -> Result<(), Errors> { - let mut soundtrack_count: i32 = 0; - let mut songs_count: u32 = 0; - let mut total_songs_count: u32 = 0; - let mut total_song_groups_count: i32 = 0; - let mut song_time_miliseconds: [i32; 6] = [0; 6]; - let mut song_group_id: i32; - - let mut soundtracks: Vec = Default::default(); - let mut songs: Vec = Default::default(); - let mut sound_groups_ids: Vec = Vec::with_capacity(84); - - let mut files_to_convert: Vec = Vec::new(); - + let mut soundtrack_count = 0; let input_path = PathBuf::from(&args.input); let music_directory = read_dir(input_path).map_err(Errors::UnknownFolder)?; + let mut soundtracks: Vec = Default::default(); + let mut song_groups: Vec> = Default::default(); + + let mut total_songs = 0; + let mut total_song_groups = 0; + + let mut files_to_convert: Vec = Default::default(); // Loop through each folders for the soundtrack struct for (i, soundtrack_dirs) in music_directory.enumerate() { @@ -105,158 +100,56 @@ fn process(args: &Args) -> Result<(), Errors> { } soundtrack_count += 1; - song_group_id = 0; - let soundtrack_name_str = soundtrack - .file_name() - .map_or(OsStr::new("Unknown soundtrack"), |f| f) - .to_string_lossy() - .trim() - .ascii_chars() - .to_string(); - - // Convert the folder name into 2 bytes - let mut soundtrack_name = soundtrack_name_str - .bytes() - .map(|b| [b, 0]) - .collect::>(); - // Max value of 32 - soundtrack_name.resize(32, [0; 2]); - - let mut song_name: [[u8; 2]; 192] = [[0; 2]; 192]; - let files = read_dir(soundtrack) - .map_err(Errors::UnknownIO)? - .collect::>(); - - // Loop through each files in chunk of 6 (max songs allowed in a song group) - files.chunks(6).for_each(|song_files| { - let mut song_id: [i32; 6] = [0; 6]; - - song_group_id += 1; - sound_groups_ids.push(total_song_groups_count); - total_song_groups_count += 1; - - let mut char_count = 0; - song_time_miliseconds = [0; 6]; - - for (g, f) in song_files.iter().enumerate() { - let song = f.as_ref().unwrap(); - let song_path = song.path(); - - // Ignore non files for song groups - if !song_path.is_file() { - continue; - } - - song_id[g] = total_songs_count as i32; - song_time_miliseconds[g] = match get_duration(song_path) { - Ok(s) => s, - Err(e) => { - eprintln!("\x1b[0;31mFailed to get duration: {}\x1b[0;20m", e); - 0 - } - }; - - songs_count += 1; - total_songs_count += 1; - - let filepath = song.path(); - let filename = filepath - .file_stem() - .map_or(OsStr::new("Unknown track"), |f| f) - .to_string_lossy() - .ascii_chars() - .to_string(); - - let mut name = filename.trim().bytes().collect::>(); - name.resize(32, 0); - - for b in name.iter() { - song_name[char_count] = [*b, 0]; - char_count += 1; - } - - files_to_convert.push(MusicFile { - path: song.path(), - soundtrack_name: soundtrack_name_str.clone(), - soundtrack_index: 0, - index: total_songs_count - 1, - }); - } - - let s = Song { - magic: 200819, - id: song_group_id - 1, - ipadding: 0, - soundtrack_id: i as i32, - song_id, - song_time_miliseconds, - song_name, - cpadding: [char::MIN; 16], - }; - - songs.push(s); - - song_name = [[0; 2]; 192]; - }); - - let mut total_time_miliseconds: i32 = 0; - - for s in &songs { - if s.soundtrack_id == i as i32 { - total_time_miliseconds += s.song_time_miliseconds.iter().sum::() - } - } - - sound_groups_ids.resize(84, 0); - let song_groups_ids: [i32; 84] = sound_groups_ids - .clone() - .try_into() - .map_err(|_| Errors::SkillIssue())?; - - let st = Soundtrack { - magic: 136049, - id: i as i32, - num_songs: songs_count, - song_groups_ids: song_groups_ids, - total_time_miliseconds, - name: soundtrack_name - .try_into() - .map_err(|_| Errors::SkillIssue())?, - padding: [char::MIN; 24], - }; + let st; + let song_group; + let mut unprocessed_files; + ( + st, + song_group, + total_songs, + total_song_groups, + unprocessed_files, + ) = process_soundtrack( + soundtrack, + i, + soundtrack_count, + total_songs, + total_song_groups, + )?; soundtracks.push(st); - sound_groups_ids = Vec::with_capacity(84); - songs_count = 0; + song_groups.push(song_group); + files_to_convert.append(&mut unprocessed_files); } let mut soundtrack_ids: [i32; 100] = [0; 100]; + for i in 0..soundtrack_count { - soundtrack_ids[i as usize] = i; + soundtrack_ids[i] = i as i32; } let header = Header { - magic: 0001, - num_soundtracks: soundtrack_count, - next_soundtrack_id: soundtrack_count + 1, + magic: 1, + num_soundtracks: soundtrack_count as i32, + next_soundtrack_id: (soundtrack_count + 1) as i32, soundtrack_ids, - next_song_id: (songs_count as i32), + next_song_id: (total_songs + 1) as i32, padding: [char::MIN; 24], }; - if files_to_convert.len() == 0 { + if files_to_convert.is_empty() { return Err(Errors::NoFileToConvert()); } - write_database(&args.output, header, soundtracks, songs)?; + write_database(&args.output, header, soundtracks, song_groups)?; - for f in files_to_convert { - let percentage: f64 = ((f.index + 1) as f64 / total_songs_count as f64) * 100.0; + for (i, f) in files_to_convert.iter().enumerate() { + let percentage: f64 = ((i + 1) as f64 / total_songs as f64) * 100.0; print!( "{}{}\r{:3}% [{}{}] {:3}/{}", - "\x1B[1A", - "\x1B[K", + Ansi::CursorUp, + Ansi::ClearLine, percentage as usize, { let mut bar = "=".repeat(percentage as usize / 3); @@ -266,31 +159,26 @@ fn process(args: &Args) -> Result<(), Errors> { bar }, " ".repeat(100 / 3 - percentage as usize / 3), - f.index + 1, - total_songs_count + i + 1, + total_songs ); print!( "{}\r{}Processing {} - {}", - "\x1B[1B", - "\x1B[K", - f.soundtrack_name, - f.path - .file_stem() - .map_or(OsStr::new("Unknown track"), |f| f) + Ansi::CursorDown, + Ansi::ClearLine, + f.parent() + .and_then(|f| f.file_stem()) + .unwrap_or_else(|| OsStr::new("Unknown soundtrack")) + .to_string_lossy(), + f.file_stem() + .unwrap_or_else(|| OsStr::new("Unknown track")) .to_string_lossy() ); stdout().flush().map_err(Errors::UnknownIO)?; - convert_to_wma( - f.path, - &args.output, - args.bitrate, - &args.codec, - f.soundtrack_index as usize, - f.index as usize, - )?; + convert_to_wma(f, &args.output, args.bitrate, &args.codec, i)?; } print!("\x1B[1A\x1B[K\r\x1B[K Done."); @@ -298,6 +186,173 @@ fn process(args: &Args) -> Result<(), Errors> { Ok(()) } +fn build_songs_group( + chunks: &[DirEntry], + song_group_id: usize, + soundtrack_id: usize, + total_songs: usize, +) -> Result<(SongGroup, Vec), Errors> { + let mut files_to_convert: Vec = Default::default(); + let mut songs: Vec = Default::default(); + let mut song_group: SongGroup = SongGroup::default(); + + // Loop through each files in chunk of 6 (max songs allowed in a song group) + chunks.iter().enumerate().for_each(|(i, song)| { + let song_path = song.path(); + + // Ignore non files for song groups + if !song_path.is_file() { + return; + } + + let song_id: i32 = (total_songs - chunks.len() + i) as i32; + + let song_time_miliseconds = match get_duration(song_path) { + Ok(s) => s, + Err(e) => { + eprintln!("\x1b[0;31mFailed to get duration: {}\x1b[0;20m", e); + 0 + } + }; + + let filepath = song.path(); + let filename = filepath + .file_stem() + .unwrap_or_else(|| OsStr::new("Unknown track")) + .to_string_lossy() + .ascii_chars() + .to_string(); + + let mut name = filename.trim().bytes().collect::>(); + name.resize(32, 0); + + let mut song_name: [[u8; 2]; 32] = [[0; 2]; 32]; + + for (char_count, b) in name.iter().enumerate() { + song_name[char_count] = [*b, 0]; + } + + let s = Song { + song_id, + song_time_miliseconds, + song_name, + }; + + songs.push(s); + files_to_convert.push(song.path()); + }); + + for s in songs.chunks(6) { + let empty_song = Song::default(); + let mut empty_song_groups = s.to_vec(); + empty_song_groups.resize(6, empty_song); + + song_group = SongGroup { + magic: 200819, + soundtrack_id: soundtrack_id as i32, + id: song_group_id as i32, + ipadding: 0, + songs: empty_song_groups.try_into().unwrap(), + cpadding: [char::MIN; 16], + }; + } + + Ok((song_group, files_to_convert)) +} + +fn process_soundtrack( + soundtrack: PathBuf, + soundtrack_index: usize, + soundtrack_count: usize, + mut total_songs: usize, + total_song_groups: usize, +) -> Result<(Soundtrack, Vec, usize, usize, Vec), Errors> { + let mut files_to_convert: Vec = Default::default(); + + let mut songs_count = 0; + + let mut song_groups: Vec = Default::default(); + let mut song_groups_ids: Vec = Vec::with_capacity(84); + + let mut song_group_id = total_song_groups; + + let soundtrack_name_str = soundtrack + .file_name() + .unwrap_or_else(|| OsStr::new("Unknown soundtrack")) + .to_string_lossy() + .trim() + .ascii_chars() + .to_string(); + + // Convert the folder name into 2 bytes + let mut soundtrack_name = soundtrack_name_str + .bytes() + .map(|b| [b, 0]) + .collect::>(); + + // Max value of 32 + soundtrack_name.resize(32, [0; 2]); + + let files = read_dir(soundtrack)? + .filter_map(|x| x.ok()) + .collect::>(); + + let mut total_time_miliseconds: i32 = 0; + + for chunk in files.chunks(6) { + total_songs += chunk.len(); + songs_count += chunk.len(); + + let (songs, mut unconverted_files) = + build_songs_group(chunk, song_group_id, soundtrack_index, total_songs)?; + + songs.songs.iter().for_each(|x| { + if songs.soundtrack_id == soundtrack_index as i32 { + total_time_miliseconds += x.song_time_miliseconds; + } + }); + + song_groups.push(songs); + song_group_id += 1; + files_to_convert.append(&mut unconverted_files); + } + + for s in &song_groups { + song_groups_ids.push(s.id); + } + + song_groups_ids.resize(84, 0); + let song_groups_ids: [i32; 84] = song_groups_ids + .clone() + .try_into() + .map_err(|_| Errors::SkillIssue())?; + + let st = Soundtrack { + magic: 136049, + id: soundtrack_index as i32, + num_songs: songs_count as u32, + song_groups_ids, + total_time_miliseconds, + name: soundtrack_name + .try_into() + .map_err(|_| Errors::SkillIssue())?, + padding: [char::MIN; 24], + }; + + let mut soundtrack_ids: [i32; 100] = [0; 100]; + for i in 0..soundtrack_count { + soundtrack_ids[i] = i as i32; + } + + Ok(( + st, + song_groups, + total_songs, + song_group_id, + files_to_convert, + )) +} + fn get_duration(path: PathBuf) -> Result { let output = Command::new("ffprobe") .args([ @@ -319,18 +374,16 @@ fn get_duration(path: PathBuf) -> Result { } fn convert_to_wma( - input: PathBuf, + input: &Path, output: &String, bitrate: i16, codec: &Codec, - soundtrack_index: usize, song_index: usize, ) -> Result<(), Errors> { - let binding = input.into_os_string(); + let binding = input.as_os_str(); let input = binding.to_str().unwrap(); - fs::create_dir_all(format!("{}/{:0>4}", output, soundtrack_index)) - .map_err(Errors::UnknownIO)?; + fs::create_dir_all(format!("{}/0000", output)).map_err(Errors::UnknownIO)?; Command::new("ffmpeg") .args([ @@ -349,10 +402,7 @@ fn convert_to_wma( "-map", "0:a", "-y", - &format!( - "{}/{:0>4}/{:0>8x}.wma", - output, soundtrack_index, song_index - ), + &format!("{}/0000/{:0>8x}.wma", output, song_index), ]) .output() .map_err(Errors::MissingFfmpeg)?; @@ -364,7 +414,7 @@ fn write_database( output: &String, header: Header, soundtracks: Vec, - songs: Vec, + songs: Vec>, ) -> Result<(), Errors> { fs::create_dir_all(format!("{}/", &output)).map_err(Errors::UnknownIO)?; let mut database = File::create(format!("{}/ST.DB", &output)).map_err(Errors::UnknownIO)?; @@ -374,10 +424,33 @@ fn write_database( database.write_all(soundtracks.as_bytes())?; for _ in 0..100 - &soundtracks.len() { - database.write_all(&[0 as u8; 512])?; + database.write_all(&[0_u8; 512])?; } - database.write_all(songs.as_bytes())?; + for s in songs { + for s in s { + database.write_all(s.magic.as_bytes())?; + database.write_all(s.soundtrack_id.as_bytes())?; + database.write_all(s.id.as_bytes())?; + database.write_all(s.ipadding.as_bytes())?; + + for x in s.songs.chunks(6) { + for chunk in x { + database.write_all(chunk.song_id.as_bytes())?; + } + + for chunk in x { + database.write_all(chunk.song_time_miliseconds.as_bytes())?; + } + + for chunk in x { + database.write_all(chunk.song_name.as_bytes())?; + } + } + + database.write_all(s.cpadding.as_bytes())?; + } + } Ok(()) } diff --git a/src/utils.rs b/src/utils.rs index fd5721d..807295e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,25 +1,16 @@ -use std::path::PathBuf; - use zerocopy::{Immutable, IntoBytes}; -pub struct MusicFile { - pub path: PathBuf, - pub soundtrack_index: u32, - pub soundtrack_name: String, - pub index: u32, -} - #[derive(clap::ValueEnum, Debug, Clone)] pub enum Codec { Wmav1, Wmav2, } -impl ToString for Codec { - fn to_string(&self) -> String { +impl std::fmt::Display for Codec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Codec::Wmav1 => String::from("wmav1"), - Codec::Wmav2 => String::from("wmav2"), + Codec::Wmav1 => write!(f, "wmav1"), + Codec::Wmav2 => write!(f, "wmav2"), } } } @@ -48,16 +39,22 @@ pub struct Soundtrack { pub padding: [char; 24], } -#[derive(Debug, Immutable, IntoBytes)] +#[derive(Debug, Immutable, IntoBytes, Clone, Copy)] #[repr(C)] pub struct Song { + pub song_id: i32, + pub song_time_miliseconds: i32, + pub song_name: [[u8; 2]; 32], +} + +#[derive(Debug, Immutable, IntoBytes, Clone, Copy)] +#[repr(C)] +pub struct SongGroup { pub magic: i32, pub soundtrack_id: i32, pub id: i32, pub ipadding: i32, - pub song_id: [i32; 6], - pub song_time_miliseconds: [i32; 6], - pub song_name: [[u8; 2]; 192], + pub songs: [Song; 6], pub cpadding: [char; 16], } @@ -94,14 +91,39 @@ impl Default for Song { #[inline] fn default() -> Song { Song { + song_id: 0, + song_time_miliseconds: 0, + song_name: [[0; 2]; 32], + } + } +} + +impl Default for SongGroup { + #[inline] + fn default() -> SongGroup { + SongGroup { magic: 0, soundtrack_id: 0, id: 0, ipadding: 0, - song_id: [0; 6], - song_time_miliseconds: [0; 6], - song_name: [[0; 2]; 192], + songs: [Song::default(); 6], cpadding: [char::MIN; 16], } } } + +pub enum Ansi { + ClearLine, + CursorUp, + CursorDown, +} + +impl std::fmt::Display for Ansi { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Ansi::ClearLine => write!(f, "\x1B[K"), + Ansi::CursorUp => write!(f, "\x1B[1A"), + Ansi::CursorDown => write!(f, "\x1B[1B"), + } + } +}