diff --git a/src/main.rs b/src/main.rs index b967d55..099e91f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ enum Commands{ DownloadGroupInventoryJson(DownloadGroupInventoryJsonSubcommand), CreateAsset(CreateAssetSubcommand), CreateAssetMedia(CreateAssetMediaSubcommand), + CreateAssetMedias(CreateAssetMediasSubcommand), UploadAsset(UpdateAssetSubcommand), UploadAssetMedia(UpdateAssetMediaSubcommand), UploadPlace(UpdatePlaceSubcommand), @@ -128,6 +129,26 @@ struct CreateAssetMediaSubcommand{ expected_price:Option, } #[derive(Args)] +/// Automatically detect the media type from file extension and generate asset name and description +struct CreateAssetMediasSubcommand{ + #[arg(long,group="api_key",required=true)] + api_key_literal:Option, + #[arg(long,group="api_key",required=true)] + api_key_envvar:Option, + #[arg(long,group="api_key",required=true)] + api_key_file:Option, + #[arg(long)] + asset_type:AssetType, + #[arg(long,group="creator",required=true)] + creator_user_id:Option, + #[arg(long,group="creator",required=true)] + creator_group_id:Option, + /// Expected price limits how much robux can be spent to create the asset (defaults to 0) + #[arg(long)] + expected_price:Option, + input_files:Vec, +} +#[derive(Args)] struct UpdateAssetSubcommand{ #[arg(long)] asset_id:AssetID, @@ -424,6 +445,20 @@ async fn main()->AResult<()>{ description:subcommand.description.unwrap_or_else(||String::with_capacity(0)), expected_price:subcommand.expected_price, }).await, + Commands::CreateAssetMedias(subcommand)=>create_asset_medias(CreateAssetMediasConfig{ + api_key:api_key_from_args( + subcommand.api_key_literal, + subcommand.api_key_envvar, + subcommand.api_key_file, + ).await?, + creator:match (subcommand.creator_user_id,subcommand.creator_group_id){ + (Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id.to_string()), + (None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id.to_string()), + other=>Err(anyhow!("Invalid creator {other:?}"))?, + }, + input_files:subcommand.input_files, + expected_price:subcommand.expected_price, + }).await, Commands::UploadAsset(subcommand)=>upload_asset(UploadAssetConfig{ cookie:cookie_from_args( subcommand.cookie_literal, @@ -614,6 +649,85 @@ async fn create_asset_media(config:CreateAssetMediaConfig)->AResult<()>{ Ok(()) } +struct CreateAssetMediasConfig{ + api_key:ApiKey, + description:String, + input_files:Vec, + creator:rbx_asset::cloud::Creator, + expected_price:Option, +} + +#[derive(Debug)] +enum CreateAssetMediasError{ + NoFileStem(PathBuf), + UnknownFourCC(Option>), +} +impl std::fmt::Display for CreateAssetMediasError{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for CreateAssetMediasError{} + +async fn create_asset_medias(config:CreateAssetMediasConfig)->AResult<()>{ + let context=CloudContext::new(config.api_key); + futures::stream::iter(config.input_files.into_iter() + //step 1: read file, make create request + .map(|path|{ + let config=&config; + let context=&context; + async move{ + let model_name=path.file_stem() + .and_then(std::ffi::OsStr::to_str) + .ok_or(CreateAssetMediasError::NoFileStem(path.clone()))?; + let file=tokio::fs::read(path).await?; + let asset_type=match file.get(0..4){ + Some(b"\x89PNG")=>rbx_asset::cloud::AssetType::Decal, + //jpeg + Some(b"\xFF\xD8\xFF\xE0")=>rbx_asset::cloud::AssetType::Decal, + //Some("fbx")=>rbx_asset::cloud::AssetType::Model, + //Some("ogg")=>rbx_asset::cloud::AssetType::Audio, + fourcc=>Err(CreateAssetMediasError::UnknownFourCC(fourcc.map(<[u8]>::to_owned)))?, + }; + Ok(context.create_asset(rbx_asset::cloud::CreateAssetRequest{ + assetType:asset_type, + displayName:model_name.to_owned(), + description:config.description.clone(), + creationContext:rbx_asset::cloud::CreationContext{ + creator:config.creator, + expectedPrice:Some(config.expected_price.unwrap_or(0)), + } + },file).await?) + } + })) + //parallel requests + .buffer_unordered(CONCURRENT_REQUESTS) + //step 2: poll operation until it completes (as fast as possible no exp backoff or anything just hammer roblox) + .filter_map(|create_result:AResult<_>|{ + let context=&context; + async{ + match create_result{ + Ok(operation)=>match operation.wait(context).await{ + Ok(())=>Some(operation), + Err(e)=>{ + eprintln!("operation error: {}",e); + None + }, + }, + Err(e)=>{ + eprintln!("create_asset error: {}",e); + None + }, + } + } + }) + //step 3: read decal id from operation and download it + .filter_map(|operation|{ + + }); + Ok(()) +} + struct UploadAssetConfig{ cookie:Cookie, asset_id:AssetID,