Skip to content

Commit

Permalink
add support for audio assets (#67)
Browse files Browse the repository at this point in the history
* added audio file support

* check for robux price on updates too
  • Loading branch information
blake-mealey authored Nov 16, 2021
1 parent 641584b commit 141df3a
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 5 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ chrono = "0.4"
yansi = "0.5.0"
scraper = "0.12.0"
url = "2.2.2"
base64 = "0.13.0"
Binary file added project-fixtures/dev/assets/audio-1.mp3
Binary file not shown.
75 changes: 70 additions & 5 deletions src/resource_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ use serde::{Deserialize, Serialize};
use crate::{
resources::ResourceManager,
roblox_api::{
CreateBadgeResponse, CreateDeveloperProductResponse, CreateExperienceResponse,
CreateGamePassResponse, CreateImageAssetResponse, CreatePlaceResponse,
ExperienceConfigurationModel, GetDeveloperProductResponse, GetExperienceResponse,
GetPlaceResponse, PlaceConfigurationModel, RobloxApi, UploadImageResponse,
CreateAudioAssetResponse, CreateBadgeResponse, CreateDeveloperProductResponse,
CreateExperienceResponse, CreateGamePassResponse, CreateImageAssetResponse,
CreatePlaceResponse, ExperienceConfigurationModel, GetCreateAudioAssetPriceResponse,
GetDeveloperProductResponse, GetExperienceResponse, GetPlaceResponse,
PlaceConfigurationModel, RobloxApi, UploadImageResponse,
},
roblox_auth::RobloxAuth,
};
Expand All @@ -34,6 +35,7 @@ pub mod resource_types {
pub const BADGE_ICON: &str = "badgeIcon";
pub const ASSET_ALIAS: &str = "assetAlias";
pub const IMAGE_ASSET: &str = "imageAsset";
pub const AUDIO_ASSET: &str = "audioAsset";
}

pub const SINGLETON_RESOURCE_ID: &str = "singleton";
Expand Down Expand Up @@ -248,6 +250,18 @@ struct ImageAssetOutputs {
decal_asset_id: AssetId,
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct AudioAssetInputs {
file_path: String,
file_hash: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct AudioAssetOutputs {
asset_id: AssetId,
}

pub struct RobloxResourceManager {
roblox_api: RobloxApi,
project_path: PathBuf,
Expand All @@ -266,10 +280,39 @@ impl ResourceManager for RobloxResourceManager {
fn get_create_price(
&mut self,
resource_type: &str,
_resource_inputs: serde_yaml::Value,
resource_inputs: serde_yaml::Value,
) -> Result<Option<u32>, String> {
match resource_type {
resource_types::BADGE => Ok(Some(100)),
resource_types::AUDIO_ASSET => {
let inputs = serde_yaml::from_value::<AudioAssetInputs>(resource_inputs)
.map_err(|e| format!("Failed to deserialize inputs: {}", e))?;

let GetCreateAudioAssetPriceResponse {
price, can_afford, ..
} = self.roblox_api.get_create_audio_asset_price(
self.project_path.join(inputs.file_path).as_path(),
)?;

// TODO: Add support for failing early like this for all other resource types (e.g. return the price and current balance from this function)
if !can_afford {
return Err(format!("You do not have enough Robux to create an audio asset with the price of {}", price));
}

Ok(Some(price))
}
_ => Ok(None),
}
}

fn get_update_price(
&mut self,
resource_type: &str,
resource_inputs: serde_yaml::Value,
_resource_outputs: serde_yaml::Value,
) -> Result<Option<u32>, String> {
match resource_type {
resource_types::AUDIO_ASSET => self.get_create_price(resource_type, resource_inputs),
_ => Ok(None),
}
}
Expand Down Expand Up @@ -567,6 +610,19 @@ impl ResourceManager for RobloxResourceManager {
.map_err(|e| format!("Failed to serialize outputs: {}", e))?,
))
}
resource_types::AUDIO_ASSET => {
let inputs = serde_yaml::from_value::<AudioAssetInputs>(resource_inputs)
.map_err(|e| format!("Failed to deserialize inputs: {}", e))?;

let CreateAudioAssetResponse { id } = self
.roblox_api
.create_audio_asset(self.project_path.join(inputs.file_path).as_path())?;

Ok(Some(
serde_yaml::to_value(AudioAssetOutputs { asset_id: id })
.map_err(|e| format!("Failed to serialize outputs: {}", e))?,
))
}
_ => panic!(
"Create not implemented for resource type: {}",
resource_type
Expand Down Expand Up @@ -698,6 +754,7 @@ impl ResourceManager for RobloxResourceManager {
))
}
resource_types::IMAGE_ASSET => self.create(resource_type, resource_inputs),
resource_types::AUDIO_ASSET => self.create(resource_type, resource_inputs),
_ => panic!(
"Update not implemented for resource type: {}",
resource_type
Expand Down Expand Up @@ -873,6 +930,14 @@ impl ResourceManager for RobloxResourceManager {

Ok(())
}
resource_types::AUDIO_ASSET => {
let outputs = serde_yaml::from_value::<AudioAssetOutputs>(resource_outputs)
.map_err(|e| format!("Failed to deserialize outputs: {}", e))?;

self.roblox_api.archive_asset(outputs.asset_id)?;

Ok(())
}
_ => panic!(
"Delete not implemented for resource type: {}",
resource_type
Expand Down
36 changes: 36 additions & 0 deletions src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ pub trait ResourceManager {
resource_inputs: serde_yaml::Value,
) -> Result<Option<u32>, String>;

fn get_update_price(
&mut self,
resource_type: &str,
resource_inputs: serde_yaml::Value,
resource_outputs: serde_yaml::Value,
) -> Result<Option<u32>, String>;

fn create(
&mut self,
resource_type: &str,
Expand Down Expand Up @@ -496,6 +503,35 @@ impl ResourceGraph {
}
};

match resource_manager.get_update_price(
&resource.resource_type,
inputs.clone(),
outputs.clone(),
) {
Ok(Some(price)) if price > 0 => {
if allow_purchases {
logger::log("");
logger::log(Paint::yellow(format!(
"{} Robux will be charged from your account.",
price
)))
} else {
return OperationResult::Skipped(format!(
"Resource would cost {} Robux to update. Give Rocat permission to make purchases with --allow-purchases.",
price
));
}
}
Err(e) => {
return OperationResult::Failed(format!(
"Unable to get update price: {}",
e
))
}
Ok(None) => {}
Ok(Some(_)) => {}
};

match resource_manager.update(&resource.resource_type, inputs, outputs) {
Ok(Some(outputs)) => {
let outputs = match serde_yaml::from_value::<BTreeMap<String, OutputValue>>(
Expand Down
89 changes: 89 additions & 0 deletions src/roblox_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ pub struct CreateImageAssetResponse {
pub backing_asset_id: AssetId,
}

#[derive(Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct GetCreateAudioAssetPriceResponse {
pub price: u32,
pub balance: u32,
pub can_afford: bool,
}

#[derive(Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct CreateAudioAssetResponse {
pub id: AssetId,
}

#[derive(Serialize, Deserialize, Clone)]
pub enum ExperienceGenre {
All,
Expand Down Expand Up @@ -1161,6 +1175,81 @@ impl RobloxApi {
.into_json::<CreateImageAssetResponse>()
.map_err(|e| format!("Failed to deserialize create image asset response: {}", e))?;

if !model.success {
return Err("Failed to create image asset (unknown error)".to_owned());
}

Ok(model)
}

pub fn get_create_audio_asset_price(
&mut self,
file_path: &Path,
) -> Result<GetCreateAudioAssetPriceResponse, String> {
let data = fs::read(file_path).map_err(|e| {
format!(
"Unable to read audio asset file: {}\n\t{}",
file_path.display(),
e
)
})?;

let file_name = format!(
"Audio/{}",
file_path.file_stem().map(OsStr::to_str).flatten().unwrap()
);

let res = ureq::post("https://publish.roblox.com/v1/audio/verify")
.query("name", &file_name)
.set("Content-Type", "*/*")
.set_auth(AuthType::CookieAndCsrfToken, &mut self.roblox_auth)?
.send_json(json!({
"name": file_name,
"fileSize": data.len(),
"file": base64::encode(data)
}));

let response = Self::handle_response(res)?;
let model = response
.into_json::<GetCreateAudioAssetPriceResponse>()
.map_err(|e| {
format!(
"Failed to deserialize get create audio asset price response: {}",
e
)
})?;

Ok(model)
}

pub fn create_audio_asset(
&mut self,
file_path: &Path,
) -> Result<CreateAudioAssetResponse, String> {
let data = fs::read(file_path).map_err(|e| {
format!(
"Unable to read audio asset file: {}\n\t{}",
file_path.display(),
e
)
})?;

let file_name = format!(
"Audio/{}",
file_path.file_stem().map(OsStr::to_str).flatten().unwrap()
);
let res = ureq::post("https://publish.roblox.com/v1/audio")
.set_auth(AuthType::CookieAndCsrfToken, &mut self.roblox_auth)?
.send_json(json!({
"name": file_name,
"file": base64::encode(data)
}));

let response = Self::handle_response(res)?;
let model = response
.into_json::<CreateAudioAssetResponse>()
.map_err(|e| format!("Failed to deserialize create audio asset response: {}", e))?;

Ok(model)
}

Expand Down
2 changes: 2 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,11 +428,13 @@ pub fn get_desired_graph(
Some(Some("bmp" | "gif" | "jpeg" | "jpg" | "png" | "tga")) => {
resource_types::IMAGE_ASSET
}
Some(Some("ogg" | "mp3")) => resource_types::AUDIO_ASSET,
_ => return Err(format!("Unable to determine asset type for file: {}", file)),
};

let alias_folder = match resource_type {
resource_types::IMAGE_ASSET => "Images",
resource_types::AUDIO_ASSET => "Audio",
_ => unreachable!(),
};

Expand Down

0 comments on commit 141df3a

Please sign in to comment.