use super::model2::*; use std::collections::HashMap; use std::convert::TryFrom; use std::path::PathBuf; use std::path::Path; use std::sync::Arc; pub trait OsuBeatmapSets { fn beatmap_sets(&self) -> Vec>; fn boxed(self) -> Box; } pub trait OsuBeatmapSet { fn beatmaps(&self) -> Vec; fn boxed(self) -> Box; } #[derive(Debug, Clone, new)] pub struct OsuBeatmapInfoHolder { pub ascii: BasicSongInfo, pub unicode: BasicSongInfo, pub beatmapset_id: u64, pub background: Option, pub audio: PathBuf, pub beatmap: PathBuf, pub extensions: (Option, Option), } #[derive(Debug, Clone, new)] pub struct OsuBeatmapInfoHolderSimple { pub info: BasicSongInfo, pub beatmapset_id: u64, pub background: Option, pub audio: PathBuf, pub beatmap: PathBuf, pub extensions: (Option, Option), } impl From<(OsuBeatmapInfoHolder, bool)> for OsuBeatmapInfoHolderSimple { fn from((other, unicode_support): (OsuBeatmapInfoHolder, bool)) -> Self { Self::new( if unicode_support { other.unicode } else { other.ascii }, other.beatmapset_id, other.background, other.audio, other.beatmap, other.extensions, ) } } #[derive(Debug, Clone, new)] pub struct OsuBeatmapInfoExtracted { pub ascii_opt: Option, pub unicode: BasicSongInfo, pub background: Option, pub audio: String, } #[derive(Debug, Clone, new)] pub struct BasicSongInfo { pub title: String, pub artist: String, } impl BasicSongInfo { pub fn filter_ascii(&self) -> Self { Self::new( self.title.chars().filter(char::is_ascii).collect(), self.artist.chars().filter(char::is_ascii).collect(), ) } } impl TryFrom<&PathBuf> for OsuBeatmapInfoExtracted { type Error = String; fn try_from(path: &PathBuf) -> Result { Self::try_from(&std::fs::read_to_string(path).map_err(|e| format!("{:?}", e))?) } } fn assemble_hierarchy(items: Vec<(bool, T)>) -> Vec> { let mut nested: Vec> = vec![]; let mut buffer: Vec = vec![]; for (head, item) in items { if head { if !buffer.is_empty() { nested.push(buffer); } buffer = vec![]; } buffer.push(item); } if !buffer.is_empty() { nested.push(buffer); } nested } fn get_osu_beatmap_sections(beatmap_string: &str) -> HashMap> { let beatmap_string_unixnewlines = beatmap_string.replace('\r', ""); let beatmap_lines: Vec<(bool, &str)> = beatmap_string_unixnewlines .split('\n') .map(|line| { let trimmed_line = line.trim(); ( trimmed_line.starts_with('[') && trimmed_line.ends_with(']'), line, ) }) .skip_while(|t| !t.0) .collect(); let beatmap_sections_vec: Vec> = assemble_hierarchy(beatmap_lines); let beatmap_sections: HashMap> = beatmap_sections_vec .iter() .map(|vec| { vec.iter() .filter(|line| !line.is_empty()) .collect::>() }) .map(|vec| { let section_syntax = vec.first().unwrap().trim().to_string(); let section_key = section_syntax[1..(section_syntax.len() - 1)] .trim() .to_string(); ( section_key.to_lowercase(), vec.iter().skip(1).map(|s| s.to_string()).collect(), ) }) .collect(); beatmap_sections } impl TryFrom<&String> for OsuBeatmapInfoExtracted { type Error = String; fn try_from(beatmap_string: &String) -> Result { let beatmap_sections: HashMap> = get_osu_beatmap_sections(beatmap_string); // println!("{:#?}", beatmap_sections); // println!("{:#?}", beatmap_sections.get("events")); let background: Option = beatmap_sections .get("events") .and_then(|events_vec| { events_vec .iter() .filter(|line| line.starts_with("0,0,\"")) .map(|line| line.split('"').nth(1).unwrap()) .next() }) .map(|item| item.to_string()); let general: HashMap = beatmap_sections .get("general") .unwrap() .iter() .filter(|line| line.contains(": ")) .map(|line| { let mut spl = line.splitn(2, ": "); ( spl.next().unwrap().to_string(), spl.next().unwrap().to_string(), ) }) .collect(); // println!("{:#?}", general); let audio_filename = general.get("AudioFilename").unwrap(); let metadata: HashMap = beatmap_sections .get("metadata") .unwrap() .iter() .filter(|line| line.contains(':')) .map(|line| { let mut spl = line.splitn(2, ':'); ( spl.next().unwrap().to_string(), spl.next().unwrap().to_string(), ) }) .collect(); // println!("{:#?}", metadata); let title = metadata.get("Title").unwrap(); let artist = metadata.get("Artist").unwrap(); let title_unicode_opt = metadata.get("TitleUnicode"); let artist_unicode_opt = metadata.get("ArtistUnicode"); let info_unknown = BasicSongInfo::new(title.to_string(), artist.to_string()); let info_unicode_opt = title_unicode_opt.and_then(|title_unicode| { artist_unicode_opt.map(|artist_unicode| { BasicSongInfo::new(title_unicode.to_string(), artist_unicode.to_string()) }) }); let (info_ascii, info_unicode) = if let Some(info_unicode_) = info_unicode_opt { (Some(info_unknown), info_unicode_) } else { (None, info_unknown) }; // println!("{:#?}", title); // println!("{:#?}", title_unicode_opt); // println!("{:#?}", artist); // println!("{:#?}", artist_unicode_opt); // println!("{:#?}", audio_filename); // println!("{:#?}", background); // panic!(); Ok(Self::new( info_ascii, info_unicode, background, audio_filename.to_string(), )) } } #[derive(Debug, Clone, new)] pub struct Osu40BeatmapSetsReader { pub beatmapsets_folder: PathBuf, } #[derive(Debug, Clone, new)] pub struct Osu50BeatmapSetsReader { pub hash_resolver: Arc, pub connection: Arc, } #[derive(Debug, Clone, new)] pub struct Osu50HashResolver { pub folder: PathBuf, } impl Osu50HashResolver { pub fn resolve(&self, hash: &str) -> Result { let final_path_buf = self .folder .join(hash.chars().take(1).collect::()) .join(hash.chars().take(2).collect::()) .join(hash); if final_path_buf.is_file() { Ok(final_path_buf) } else { Err(format!("{:?} is not a hashed file", final_path_buf)) } } } impl TryFrom<&PathBuf> for Osu40BeatmapSetsReader { type Error = String; fn try_from(path: &PathBuf) -> Result { if !path.is_dir() { return Err(format!("{:?} is not a directory", path)); } let songs_path = path.join("Songs"); if !songs_path.is_dir() { return Err(format!( "{:?} directory was not found in your osu!classic directory", songs_path )); } Ok(Self::new(songs_path)) } } impl OsuBeatmapSets for Osu40BeatmapSetsReader { fn boxed(self) -> Box { Box::new(self) } fn beatmap_sets(&self) -> Vec> { let folders: Vec = self .beatmapsets_folder .read_dir() .unwrap() .map(|entry_opt| entry_opt.unwrap()) .map(|entry| entry.path()) .filter(|path| path.is_dir()) .filter(|path| { path.file_name() .unwrap() .to_str() .unwrap() .to_string() .split(' ') .next() .unwrap() .parse::() .is_ok() }) .collect(); folders .iter() .map(|folder| Osu40BeatmapSet::new(folder.clone())) .map(|boxable| boxable.boxed()) .collect() } } #[derive(Debug, Clone, new)] pub struct Osu40BeatmapSet { pub beatmap_folder: PathBuf, } impl OsuBeatmapSet for Osu40BeatmapSet { fn boxed(self) -> Box { Box::new(self) } fn beatmaps(&self) -> Vec { let beatmapset_id: u64 = self .beatmap_folder .file_name() .unwrap() .to_str() .unwrap() .split(' ') .next() .unwrap() .parse::() .unwrap(); let osu_files: Vec = self .beatmap_folder .read_dir() .unwrap() .map(|entry_opt| entry_opt.unwrap()) .map(|entry| entry.path()) .filter(|path| path.is_file()) .filter(|path| { path.extension() .map(|ext| ext.to_str().unwrap() == "osu") .unwrap_or(false) }) .collect(); let osu_infos_extracted: Vec<(&PathBuf, OsuBeatmapInfoExtracted)> = osu_files .iter() .map(|path| (path, OsuBeatmapInfoExtracted::try_from(path).unwrap())) .collect(); osu_infos_extracted .iter() .filter_map( |(path, beatmap_info): &(&PathBuf, OsuBeatmapInfoExtracted)| { let background = beatmap_info.background.clone().and_then(|bkg| { let bkg_test = self.beatmap_folder.join(bkg); if bkg_test.is_file() { Some(bkg_test) } else { None } }); let audio_opt = { let aud = beatmap_info.audio.clone(); let aud_test = self.beatmap_folder.join(aud); if aud_test.is_file() { Some(aud_test) } else { None } }; let extensions = ( audio_opt.as_ref().and_then(|audio| { PathBuf::from(&audio) .extension() .and_then(|x| x.to_str().map(|y| y.to_lowercase())) }), background.as_ref().and_then(|bkg| { PathBuf::from(&bkg) .extension() .and_then(|x| x.to_str().map(|y| y.to_lowercase())) }), ); audio_opt.and_then(|audio| { if audio.is_file() { Some(OsuBeatmapInfoHolder::new( beatmap_info .ascii_opt .clone() .unwrap_or_else(|| beatmap_info.unicode.filter_ascii()), beatmap_info.unicode.clone(), beatmapset_id, background, audio, PathBuf::from(path), extensions, )) } else { None } }) }, ) .collect() } } impl TryFrom<&PathBuf> for Osu50BeatmapSetsReader { type Error = String; fn try_from(path: &PathBuf) -> Result { if !path.is_dir() { return Err(format!("{:?} is not a directory", path)); } let files_path = path.join("files"); if !files_path.is_dir() { return Err(format!( "{:?} directory was not found in your osu!lazer directory", files_path )); } let client_path = path.join("client.db"); if !client_path.is_file() { return Err(format!( "{:?} file was not found in your osu!lazer directory", files_path )); } let connection = rusqlite::Connection::open_with_flags( client_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX, ) .map_err(|x| format!("{:?}", x))?; let mut connection_memory = rusqlite::Connection::open_in_memory().unwrap(); rusqlite::backup::Backup::new(&connection, &mut connection_memory) .unwrap() .run_to_completion(100000, std::time::Duration::from_millis(0), None) .unwrap(); Ok(Self::new( Arc::new(Osu50HashResolver::new(files_path)), Arc::new(connection_memory), )) } } impl OsuBeatmapSets for Osu50BeatmapSetsReader { fn boxed(self) -> Box { Box::new(self) } fn beatmap_sets(&self) -> Vec> { let mut stmt = self .connection .prepare(PRP_STMT_OSU_LAZER_LIST_BEATMAPSETS) .unwrap(); let beatmap_set_db_listing_item: Vec = stmt .query_map(rusqlite::params![], |row| { Ok(Osu50BeatmapSetDbListingItem::new( row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, (row.get(5)?, row.get(6)?), (row.get(7)?, row.get(8)?), )) }) .unwrap() .filter_map(|x| x.ok()) .collect(); beatmap_set_db_listing_item .into_iter() .map(|beatmapset_db_info| { Box::new(Osu50BeatmapSet::new( self.hash_resolver.clone(), self.connection.clone(), beatmapset_db_info, )) as Box }) .collect() } } #[derive(Debug, Clone, new)] pub struct Osu50BeatmapSet { pub hash_resolver: Arc, pub connection: Arc, pub beatmapset_db_info: Osu50BeatmapSetDbListingItem, } impl OsuBeatmapSet for Osu50BeatmapSet { fn boxed(self) -> Box { Box::new(self) } fn beatmaps(&self) -> Vec { if let Some(audio) = self .beatmapset_db_info .audio .1 .clone() .and_then(|hash| self.hash_resolver.resolve(&hash).ok()) { let background = self .beatmapset_db_info .background .1 .clone() .and_then(|hash| self.hash_resolver.resolve(&hash).ok()); let beatmapset_id = self.beatmapset_db_info.id; let title = self.beatmapset_db_info.title.clone(); let artist = self.beatmapset_db_info.artist.clone(); let title_unicode_opt = self.beatmapset_db_info.title_unicode.clone(); let artist_unicode_opt = self.beatmapset_db_info.artist_unicode.clone(); let info_unknown = BasicSongInfo::new(title, artist); let info_unicode_opt = title_unicode_opt.and_then(|title_unicode| { artist_unicode_opt.map(|artist_unicode| { BasicSongInfo::new(title_unicode.to_string(), artist_unicode) }) }); let (info_ascii, info_unicode) = if let Some(info_unicode_) = info_unicode_opt { (Some(info_unknown), info_unicode_) } else { (None, info_unknown) }; let mut stmt = self .connection .prepare(PRP_STMT_OSU_LAZER_LIST_BEATMAPS_FROM_SET) .unwrap(); let beatmap_from_set_db_listing_item: Vec = stmt .query_map(rusqlite::params![beatmapset_id], |row| { Ok(Osu50BeatmapDbListingItem::new( row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, )) }) .unwrap() .filter_map(|x| x.ok()) .collect(); beatmap_from_set_db_listing_item .into_iter().filter_map(|osu_betmap_db_listing| { let cloned_info_ascii = info_ascii.clone(); let cloned_info_unicode = info_unicode.clone(); let cloned_background = background.clone(); let cloned_audio = audio.clone(); let cloned_background_0 = self.beatmapset_db_info.background.0.clone(); let cloned_audio_0 = self.beatmapset_db_info.audio.0.clone(); self.hash_resolver .resolve(&osu_betmap_db_listing.hash) .map(|beatmap_pathbuf| { OsuBeatmapInfoHolder::new( cloned_info_ascii .unwrap_or_else(|| cloned_info_unicode.filter_ascii()), cloned_info_unicode, beatmapset_id as u64, cloned_background, cloned_audio, beatmap_pathbuf, ( cloned_audio_0.clone().and_then(|aud| { PathBuf::from(&aud).extension().and_then(|x| { x.to_str().map(|y| y.to_lowercase()) }) }), cloned_background_0.clone().and_then(|bkg| { PathBuf::from(&bkg).extension().and_then(|x| { x.to_str().map(|y| y.to_lowercase()) }) }), ), ) }) .ok() }) .collect() } else { vec![] } } } impl OsuBeatmapInfoHolderSimple { pub fn build_path(&self, path: &Path, filename_template: &str) -> PathBuf { let mut filename: String = "".to_string(); let mut shift = false; for ch in filename_template.chars() { if shift { shift = false; match ch { 'a' => filename.push_str(&self.info.artist), 't' => filename.push_str(&self.info.title), 'i' => filename.push_str(&format!("{}", self.beatmapset_id)), '/' => (), _ => filename.push(ch), } } else if ch == '%' { shift = true; } else if ch == '/' { } else { filename.push(ch); } } if shift { filename.push('%'); } for ch in &['<', '>', ':', '"', '/', '\\', '|', '?', '*', '\''] { filename = filename.replace(*ch, ""); } if let (Some(audio_extension), _) = &self.extensions { filename.push('.'); filename.push_str(audio_extension); } path.join(filename) } }