update to handle generic file upload

This commit is contained in:
Brandon Thomas committed 2026-03-15 21:04:33 -04:00
1 parent 2e2c308c5f
commit 10f9a9839a
9 files changed
+82 -53

No files matched your search

@@ -1,4 +1,4 @@
pub mod generate_video;
pub mod poll_orders;
pub mod prepare_image_upload;
pub mod upload_image;
pub mod prepare_file_upload;
pub mod upload_file;
@@ -0,0 +1,2 @@
pub mod prepare_file_upload;
mod request_types;
@@ -2,7 +2,7 @@ use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
use crate::requests::prepare_image_upload::request_types::*;
use crate::requests::prepare_file_upload::request_types::*;
use crate::utils::common_headers::FIREFOX_USER_AGENT;
use chrono::Utc;
use log::info;
@@ -13,32 +13,35 @@ use wreq_util::Emulation;
const SIGNED_UPLOAD_URL: &str = "https://seedance2-pro.com/api/trpc/uploads.signedUploadUrl?batch=1";
/// Generates a material path based on the current time.
/// Format: `materials/YYYYMMDD/<unix_millis>-<random_hex>.png`
fn generate_material_path() -> String {
/// Format: `materials/YYYYMMDD/<unix_millis>-<random_hex>.<extension>`
fn generate_material_path(extension: &str) -> String {
let now = Utc::now();
let date_part = now.format("%Y%m%d").to_string();
let timestamp_millis = now.timestamp_millis();
let random_hex: u32 = rand::rng().random();
let hex_part = format!("{:08x}", random_hex);
format!("materials/{}/{}-{}.png", date_part, timestamp_millis, hex_part)
format!("materials/{}/{}-{}.{}", date_part, timestamp_millis, hex_part, extension)
}
pub struct PrepareImageUploadArgs<'a> {
pub struct PrepareFileUploadArgs<'a> {
pub session: &'a Seedance2ProSession,
/// File extension without the dot, e.g. "png", "mp4", "mp3".
pub extension: String,
}
pub struct PrepareImageUploadResponse {
/// The signed URL to upload the image to (Cloudflare R2 / S3-compatible).
pub struct PrepareFileUploadResponse {
/// The signed URL to upload the file to (Cloudflare R2 / S3-compatible).
pub upload_url: String,
/// The material path that was generated for this upload.
pub material_path: String,
}
pub async fn prepare_image_upload(args: PrepareImageUploadArgs<'_>) -> Result<PrepareImageUploadResponse, Seedance2ProError> {
let material_path = generate_material_path();
pub async fn prepare_file_upload(args: PrepareFileUploadArgs<'_>) -> Result<PrepareFileUploadResponse, Seedance2ProError> {
let material_path = generate_material_path(&args.extension);
info!("Preparing image upload with path: {}", material_path);
info!("Preparing file upload with path: {}", material_path);
let client = Client::builder()
.emulation(Emulation::Firefox143)
@@ -103,7 +106,7 @@ pub async fn prepare_image_upload(args: PrepareImageUploadArgs<'_>) -> Result<Pr
.data
.json;
Ok(PrepareImageUploadResponse {
Ok(PrepareFileUploadResponse {
upload_url,
material_path,
})
@@ -120,19 +123,41 @@ mod tests {
#[tokio::test]
#[ignore] // manually test — requires real cookies
async fn test_prepare_image_upload() -> AnyhowResult<()> {
async fn test_prepare_file_upload_png() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Trace);
let cookies = get_test_cookies()?;
let session = Seedance2ProSession::from_cookies_string(cookies);
let args = PrepareImageUploadArgs {
let args = PrepareFileUploadArgs {
session: &session,
extension: "png".to_string(),
};
let result = prepare_image_upload(args).await?;
let result = prepare_file_upload(args).await?;
println!("Upload URL: {}", result.upload_url);
println!("Material path: {}", result.material_path);
assert!(!result.upload_url.is_empty());
assert!(result.upload_url.contains("cloudflarestorage.com"));
assert!(result.material_path.starts_with("materials/"));
assert!(result.material_path.ends_with(".png"));
assert_eq!(1, 2); // NB: Intentional failure to check the response.
Ok(())
}
#[tokio::test]
#[ignore] // manually test — requires real cookies
async fn test_prepare_file_upload_mp4() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Trace);
let cookies = get_test_cookies()?;
let session = Seedance2ProSession::from_cookies_string(cookies);
let args = PrepareFileUploadArgs {
session: &session,
extension: "mp4".to_string(),
};
let result = prepare_file_upload(args).await?;
println!("Upload URL: {}", result.upload_url);
println!("Material path: {}", result.material_path);
assert!(!result.upload_url.is_empty());
assert!(result.material_path.ends_with(".mp4"));
assert_eq!(1, 2); // NB: Intentional failure to check the response.
Ok(())
@@ -1,2 +0,0 @@
pub mod prepare_image_upload;
mod request_types;
@@ -0,0 +1 @@
pub mod upload_file;
@@ -9,16 +9,16 @@ use wreq_util::Emulation;
const STATIC_BASE_URL: &str = "https://static.seedance2-pro.com";
pub struct UploadImageArgs {
/// The signed upload URL returned by `prepare_image_upload`.
pub struct UploadFileArgs {
/// The signed upload URL returned by `prepare_file_upload`.
pub upload_url: String,
/// The raw image bytes to upload.
pub image_bytes: Vec<u8>,
/// The raw file bytes to upload.
pub file_bytes: Vec<u8>,
}
pub struct UploadImageResponse {
/// The public-facing URL for the uploaded image.
pub struct UploadFileResponse {
/// The public-facing URL for the uploaded file.
/// e.g. `https://static.seedance2-pro.com/materials/20260219/1771463564512-b14bfe90.png`
pub public_url: String,
}
@@ -33,8 +33,8 @@ fn build_public_url(upload_url: &str) -> Result<String, Seedance2ProError> {
Ok(format!("{}{}", STATIC_BASE_URL, path))
}
pub async fn upload_image(args: UploadImageArgs) -> Result<UploadImageResponse, Seedance2ProError> {
info!("Uploading image to: {}", args.upload_url);
pub async fn upload_file(args: UploadFileArgs) -> Result<UploadFileResponse, Seedance2ProError> {
info!("Uploading file to: {}", args.upload_url);
let client = Client::builder()
.emulation(Emulation::Firefox143)
@@ -53,7 +53,7 @@ pub async fn upload_image(args: UploadImageArgs) -> Result<UploadImageResponse,
.header("Sec-Fetch-Mode", "cors")
.header("Sec-Fetch-Site", "cross-site")
.header("Priority", "u=4")
.body(args.image_bytes)
.body(args.file_bytes)
.send()
.await
.map_err(|err| Seedance2ProGenericApiError::WreqError(err))?;
@@ -77,7 +77,7 @@ pub async fn upload_image(args: UploadImageArgs) -> Result<UploadImageResponse,
info!("Public URL: {}", public_url);
Ok(UploadImageResponse { public_url })
Ok(UploadFileResponse { public_url })
}
#[cfg(test)]
@@ -86,8 +86,8 @@ mod tests {
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::test_utils::get_test_cookies::get_test_cookies;
use crate::test_utils::setup_test_logging::setup_test_logging;
use crate::requests::prepare_image_upload::prepare_image_upload::{
prepare_image_upload, PrepareImageUploadArgs,
use crate::requests::prepare_file_upload::prepare_file_upload::{
prepare_file_upload, PrepareFileUploadArgs,
};
use errors::AnyhowResult;
use log::LevelFilter;
@@ -95,28 +95,29 @@ mod tests {
#[tokio::test]
#[ignore] // manually test — requires real cookies and a test image
async fn test_upload_image() -> AnyhowResult<()> {
async fn test_upload_image_file() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Trace);
// Step 1: Get a signed upload URL
let cookies = get_test_cookies()?;
let session = Seedance2ProSession::from_cookies_string(cookies);
let prepare_args = PrepareImageUploadArgs {
let prepare_args = PrepareFileUploadArgs {
session: &session,
extension: "jpg".to_string(),
};
let prepare_result = prepare_image_upload(prepare_args).await?;
let prepare_result = prepare_file_upload(prepare_args).await?;
println!("Upload URL: {}", prepare_result.upload_url);
// Step 2: Read a test image
let image_bytes = fs::read("/Users/bt/dev/storyteller/artcraft/test_data/image/juno.jpg")?;
println!("Image size: {} bytes", image_bytes.len());
let file_bytes = fs::read("/Users/bt/dev/storyteller/artcraft/test_data/image/juno.jpg")?;
println!("File size: {} bytes", file_bytes.len());
// Step 3: Upload
let upload_args = UploadImageArgs {
let upload_args = UploadFileArgs {
upload_url: prepare_result.upload_url,
image_bytes,
file_bytes,
};
let result = upload_image(upload_args).await?;
let result = upload_file(upload_args).await?;
println!("Public URL: {}", result.public_url);
assert!(result.public_url.starts_with("https://static.seedance2-pro.com/materials/"));
@@ -1 +0,0 @@
pub mod upload_image;
@@ -34,10 +34,10 @@ use seedance2pro::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro::requests::generate_video::generate_video::{
generate_video, BatchCount, GenerateVideoArgs, Resolution,
};
use seedance2pro::requests::prepare_image_upload::prepare_image_upload::{
prepare_image_upload, PrepareImageUploadArgs,
use seedance2pro::requests::prepare_file_upload::prepare_file_upload::{
prepare_file_upload, PrepareFileUploadArgs,
};
use seedance2pro::requests::upload_image::upload_image::{upload_image, UploadImageArgs};
use seedance2pro::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use sqlx::Acquire;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -389,20 +389,23 @@ async fn upload_to_seedance2pro(
})?
.to_vec();
let prepare_result = prepare_image_upload(PrepareImageUploadArgs { session })
.await
.map_err(|err| {
warn!("Error preparing seedance2pro image upload: {:?}", err);
CommonWebError::ServerError
})?;
let upload_result = upload_image(UploadImageArgs {
upload_url: prepare_result.upload_url,
image_bytes,
let prepare_result = prepare_file_upload(PrepareFileUploadArgs {
session,
extension: "png".to_string(),
})
.await
.map_err(|err| {
warn!("Error uploading image to seedance2pro: {:?}", err);
warn!("Error preparing seedance2pro file upload: {:?}", err);
CommonWebError::ServerError
})?;
let upload_result = upload_file(UploadFileArgs {
upload_url: prepare_result.upload_url,
file_bytes: image_bytes,
})
.await
.map_err(|err| {
warn!("Error uploading file to seedance2pro: {:?}", err);
CommonWebError::ServerError
})?;