#![feature(iter_intersperse)] mod en; mod model; mod slug; use std::collections::{BTreeMap, HashSet}; use std::fs::{File, self}; use std::io::Write; use std::path::{Path, PathBuf}; use bytes::Bytes; use reqwest::blocking::Client; use strum::{EnumIs, EnumProperty}; use zip::write::{SimpleFileOptions, ZipWriter}; const BASE_DOWNLOAD_PATH: &'static str = "https://aleteoryx.me/downloads2/emotes/pleroma/"; trait Helpers { fn escape_name(name: &str) -> String; } #[derive(Default, serde::Serialize)] struct PackMetadata { name: String, description: String, homepage: String, files: String, src: String, src_sha256: String, license: String } fn get_stamp_asset_png(client: &Client, id: &str) -> Bytes { client .get(format!("https://storage.sekai.best/sekai-en-assets/stamp/{id}_rip/{id}.png")) .send() .expect("Couldn't download stamp asset") .error_for_status() .expect("Couldn't download stamp asset") .bytes() .expect("Couldn't download stamp asset") } fn main() { let repo_dir: PathBuf = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_owned()).into(); let client = Client::new(); // English region (global) handle_region::(&client, &repo_dir, "en"); } fn handle_region(client: &Client, repo_dir: &Path, region: &'static str) { let sekai_dir = repo_dir.join(format!("sekai_world/db_{region}_diff")); let stamps = serde_json::from_reader::<_, Vec>( File::open(sekai_dir.join("stamps.json")) .expect("Couldn't open stamps.json")) .expect("stamps.json invalid") .into_iter() .map(|a| a.to_stamp()) .collect::>(); let cache_dir = repo_dir.join("stamps_cache").join(®ion); fs::create_dir_all(&cache_dir); for stamp in &stamps { let download_path = cache_dir.join(&stamp.assetbundle_name).with_extension("png"); if !fs::exists(&download_path).unwrap() { println!("Downlading asset {}...", &stamp.assetbundle_name); fs::write(&download_path, get_stamp_asset_png(&client, &stamp.assetbundle_name)) .expect("Couldn't write out stamp cache file"); } } let character_stamps = stamps.clone().into_iter() .filter(|a| a.character.is_solo() || a.character.is_solo_extra()) .collect::>(); let text_stamps = stamps.clone().into_iter() .filter(|a| a.character.is_nobody()) .collect::>(); let name_stamps = stamps.clone().into_iter() .filter(|a| a.character.is_gcuid()) .collect::>(); let pair_stamps = stamps.clone().into_iter() .filter(|a| a.character.is_duo()) .collect::>(); let packs_dir = repo_dir.join("stamp_packs"); let out_dir = packs_dir.join("Project SEKAI").join(region); fs::create_dir_all(&out_dir); let mut packs_list: BTreeMap = BTreeMap::new(); let character_out_dir = out_dir.join("characters"); fs::create_dir_all(&character_out_dir); let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated); for cid in 1..=26 { let character = model::Character::from(cid); let character_slug = character.get_str("prefix").unwrap(); let mut emotes: BTreeMap = BTreeMap::new(); let zip_path = character_out_dir.join(&character_slug).with_extension("zip"); { let mut writer = ZipWriter::new( File::create(&zip_path).unwrap()); println!("Generating character stamp pack for {}...", character); for stamp in &character_stamps { match stamp.character { model::StampCharacter::Solo{ character_id } | model::StampCharacter::SoloExtra{ character_id, .. } if character_id as u8 == cid => { let character_slug = stamp.character.to_string() + "_" + &H::escape_name(&stamp.name); writer.start_file(character_slug.clone() + ".png", options); writer.write_all(&fs::read(cache_dir.join(&stamp.assetbundle_name).with_extension("png")).unwrap()); emotes.insert(character_slug.clone(), character_slug + ".png"); }, _ => () } } writer.finish().unwrap(); } File::create( character_out_dir .join(&character_slug) .with_extension("json")) .unwrap() .write_all(&serde_json::to_vec(&emotes).unwrap()); let pack_path = format!("Project SEKAI/{region}/characters/{character_slug}"); let pack_uri_path = pack_path.replace(" ", "%20"); packs_list.insert( pack_path.clone(), PackMetadata { name: format!("Project SEKAI ({region}): {}", character.get_str(&format!("name_{region}")).unwrap()), description: "Made possible by the sekai-viewer project. ".into(), homepage: "https://git.amehut.dev/~aleteoryx/pjsekai_emote_packs".into(), src: BASE_DOWNLOAD_PATH.to_string() + &pack_uri_path + ".zip", src_sha256: sha256::try_digest(&zip_path).unwrap(), files: pack_uri_path + ".json", license: "N/A".into() }); } File::create(packs_dir.join("manifest.json")) .unwrap() .write_all(&serde_json::to_vec_pretty(&packs_list).unwrap()); }