seedance omni video (#1148)

This commit is contained in:
Brandon Thomas
2026-03-16 08:53:47 -04:00
committed by GitHub
parent 279a79e60a
commit 8ae983ee32
23 changed files with 874 additions and 94 deletions
Generated
+1
View File
@@ -11598,6 +11598,7 @@ dependencies = [
"tts_common",
"url",
"url_config",
"url_utils",
"user_input_common",
"user_traits_component",
"users",
@@ -23,6 +23,12 @@ pub struct Seedance2p0MultiFunctionVideoGenRequest {
/// Optional reference images (reference mode). When present, takes priority over start/end frames.
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
/// Optional reference videos.
pub reference_video_media_tokens: Option<Vec<MediaFileToken>>,
/// Optional reference audio files.
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
/// Video aspect ratio / resolution.
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
@@ -31,6 +31,9 @@ pub (crate) fn estimate_video_cost_artcraft_seedance2p0(
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_video_urls: None,
reference_audio_urls: None,
use_face_blur_hack: None,
};
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
@@ -18,6 +18,8 @@ pub async fn execute_artcraft_seedance2p0(
start_frame_media_token: plan.start_frame.map(|t| t.to_owned()),
end_frame_media_token: plan.end_frame.map(|t| t.to_owned()),
reference_image_media_tokens: plan.reference_images.map(|tokens| tokens.to_owned()),
reference_video_media_tokens: None,
reference_audio_media_tokens: None,
aspect_ratio: plan.aspect_ratio,
duration_seconds: plan.duration_seconds,
batch_count: Some(plan.batch_count),
@@ -31,8 +31,23 @@ pub struct GenerateVideoArgs<'a> {
/// Optional end frame image URL (keyframe mode).
pub end_frame_url: Option<String>,
/// Optional reference image URLs (reference mode). When present, takes priority over start/end frames.
/// Optional reference image URLs (reference mode).
/// When present, takes priority over start/end frames.
pub reference_image_urls: Option<Vec<String>>,
/// Optional reference video URLs (reference mode).
/// Can be combined with reference_image_urls.
/// Videos are referenced in prompts as @video1, @video2, etc.
/// When present, takes priority over start/end frames.
pub reference_video_urls: Option<Vec<String>>,
/// Optional reference audio URLs (reference mode).
/// Audio is referenced in prompts as @audio1, @audio2, etc.
/// Sent in a separate `audioUrls` field (not in `uploadedUrls`).
pub reference_audio_urls: Option<Vec<String>>,
/// Controls the `faceBlurMode` field: true sends "on", false sends "off", None omits it.
pub use_face_blur_hack: Option<bool>,
}
impl GenerateVideoArgs<'_> {
@@ -126,12 +141,23 @@ pub struct GenerateVideoResponse {
// --- Implementation ---
pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideoResponse, Seedance2ProError> {
let is_reference_mode = args.reference_image_urls.as_ref().is_some_and(|urls| !urls.is_empty());
let has_reference_images = args.reference_image_urls.as_ref().is_some_and(|urls| !urls.is_empty());
let has_reference_videos = args.reference_video_urls.as_ref().is_some_and(|urls| !urls.is_empty());
let has_reference_audio = args.reference_audio_urls.as_ref().is_some_and(|urls| !urls.is_empty());
let is_reference_mode = has_reference_images || has_reference_videos || has_reference_audio;
let video_input_mode = if is_reference_mode { "reference" } else { "keyframe" };
let uploaded_urls: Option<Vec<String>> = if is_reference_mode {
args.reference_image_urls
let mut urls = Vec::new();
if let Some(video_urls) = args.reference_video_urls {
urls.extend(video_urls);
}
if let Some(image_urls) = args.reference_image_urls {
urls.extend(image_urls);
}
if urls.is_empty() { None } else { Some(urls) }
} else {
let mut urls = Vec::new();
if let Some(url) = args.start_frame_url {
@@ -143,6 +169,18 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
if urls.is_empty() { None } else { Some(urls) }
};
let audio_urls: Option<Vec<String>> = if has_reference_audio {
args.reference_audio_urls
} else {
None
};
let face_blur_mode = match args.use_face_blur_hack {
Some(true) => Some("on"),
Some(false) => Some("off"),
None => None,
};
let batch_count_value = args.batch_count.as_u8();
let batch_count = if batch_count_value > 1 { Some(batch_count_value) } else { None };
@@ -164,7 +202,9 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
model: "seedance-20",
duration,
mode: video_input_mode,
face_blur_mode,
uploaded_urls,
audio_urls,
batch_count,
},
},
@@ -240,12 +280,15 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
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 errors::AnyhowResult;
use log::LevelFilter;
use crate::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use crate::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
fn dummy_session() -> Seedance2ProSession {
Seedance2ProSession::from_cookies_string(String::new())
@@ -263,6 +306,9 @@ mod tests {
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_video_urls: None,
reference_audio_urls: None,
use_face_blur_hack: None,
}
}
@@ -325,6 +371,9 @@ mod tests {
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_video_urls: None,
reference_audio_urls: None,
use_face_blur_hack: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
@@ -349,6 +398,9 @@ mod tests {
start_frame_url: Some("https://static.seedance2-pro.com/materials/20260219/1771496300184-fb32e08c.jpg".to_string()),
end_frame_url: None,
reference_image_urls: None,
reference_video_urls: None,
reference_audio_urls: None,
use_face_blur_hack: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
@@ -360,7 +412,7 @@ mod tests {
#[tokio::test]
#[ignore] // manually test — requires real cookies
async fn test_generate_reference_video() -> AnyhowResult<()> {
async fn test_generate_reference_image_video() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Trace);
let session = test_session()?;
let args = GenerateVideoArgs {
@@ -375,6 +427,9 @@ mod tests {
"https://static.seedance2-pro.com/materials/20260219/1771463564512-b14bfe90.png".to_string(),
"https://static.seedance2-pro.com/materials/20260219/1771496300184-fb32e08c.jpg".to_string(),
]),
reference_video_urls: None,
reference_audio_urls: None,
use_face_blur_hack: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
@@ -383,4 +438,113 @@ mod tests {
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
Ok(())
}
#[tokio::test]
#[ignore] // manually test — requires real cookies
async fn test_generate_reference_video_only() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Trace);
let session = test_session()?;
let args = GenerateVideoArgs {
session: &session,
prompt: "Change the Video @video1 to night time.".to_string(),
resolution: Resolution::Landscape16x9,
duration_seconds: 5,
batch_count: BatchCount::One,
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_video_urls: Some(vec![
"https://static.seedance2-pro.com/materials/20260315/1773594284659-3a46d231.mp4".to_string(),
]),
reference_audio_urls: None,
use_face_blur_hack: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
println!("Order ID: {}", result.order_id);
assert!(!result.task_id.is_empty());
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
Ok(())
}
#[tokio::test]
#[ignore] // manually test — requires real cookies
async fn test_generate_reference_video_and_image() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Trace);
let session = test_session()?;
let args = GenerateVideoArgs {
session: &session,
prompt: "Put the robot in @video1 next to the house in @image1".to_string(),
resolution: Resolution::Landscape16x9,
duration_seconds: 5,
batch_count: BatchCount::One,
start_frame_url: None,
end_frame_url: None,
reference_image_urls: Some(vec![
"https://static.seedance2-pro.com/materials/20260315/1773595053724-07a1d500.png".to_string(),
]),
reference_video_urls: Some(vec![
"https://static.seedance2-pro.com/materials/20260315/1773594284659-3a46d231.mp4".to_string(),
]),
reference_audio_urls: None,
use_face_blur_hack: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
println!("Order ID: {}", result.order_id);
assert!(!result.task_id.is_empty());
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
Ok(())
}
#[tokio::test]
#[ignore] // manually test — requires real cookies and a test image
async fn test_video_ref_file_that_is_too_long() -> 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 = PrepareFileUploadArgs {
session: &session,
extension: "mp4".to_string(),
};
let prepare_result = prepare_file_upload(prepare_args).await?;
println!("Upload URL: {}", prepare_result.upload_url);
// Step 2: Read a test image
let file_bytes = fs::read("/Users/bt/Videos/Artcraft/Artcraft Best/ArtCraft Seedance Knight.mp4")?;
println!("File size: {} bytes", file_bytes.len());
// Step 3: Upload
let upload_args = UploadFileArgs {
upload_url: prepare_result.upload_url,
file_bytes,
};
let result = upload_file(upload_args).await?;
println!("Public URL: {}", result.public_url);
let args = GenerateVideoArgs {
session: &session,
prompt: "Change @video1 to night time".to_string(),
resolution: Resolution::Landscape16x9,
duration_seconds: 5,
batch_count: BatchCount::One,
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_video_urls: Some(vec![
result.public_url,
]),
reference_audio_urls: None,
use_face_blur_hack: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
println!("Order ID: {}", result.order_id);
assert!(!result.task_id.is_empty());
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
Ok(())
}
}
@@ -28,8 +28,12 @@ pub (super) struct ApiParams {
pub model: &'static str,
pub duration: String,
pub mode: &'static str,
#[serde(rename = "faceBlurMode", skip_serializing_if = "Option::is_none")]
pub face_blur_mode: Option<&'static str>,
#[serde(rename = "uploadedUrls", skip_serializing_if = "Option::is_none")]
pub uploaded_urls: Option<Vec<String>>,
#[serde(rename = "audioUrls", skip_serializing_if = "Option::is_none")]
pub audio_urls: Option<Vec<String>>,
#[serde(rename = "batchCount", skip_serializing_if = "Option::is_none")]
pub batch_count: Option<u8>,
}
@@ -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,62 @@ 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/"));
assert_eq!(1, 2); // NB: Intentional failure to check the response.
Ok(())
}
#[tokio::test]
#[ignore] // manually test — requires real cookies and a test image
async fn test_upload_video_file_that_is_too_long() -> 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 = PrepareFileUploadArgs {
session: &session,
extension: "mp4".to_string(),
};
let prepare_result = prepare_file_upload(prepare_args).await?;
println!("Upload URL: {}", prepare_result.upload_url);
// Step 2: Read a test image
let file_bytes = fs::read("/Users/bt/Videos/Artcraft/Artcraft Best/ArtCraft Seedance Knight.mp4")?;
println!("File size: {} bytes", file_bytes.len());
// Step 3: Upload
let upload_args = UploadFileArgs {
upload_url: prepare_result.upload_url,
file_bytes,
};
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;
+2
View File
@@ -17,9 +17,11 @@ path = "src/lib.rs"
workspace-build-acceleration.workspace = true
# Internal
# NB: DO NOT ALLOW ANY OTHER DEPS - THIS IS URL LOGIC ONLY
# (None)
# External
# NB: DO NOT ALLOW ANY OTHER DEPS - THIS IS URL LOGIC ONLY
url.workspace = true
[dev-dependencies]
@@ -0,0 +1,146 @@
use std::borrow::Cow;
use std::fmt;
/// A file extension that can be either a compile-time constant or a runtime-parsed value.
///
/// ```
/// use url_utils::extension::extension::Extension;
///
/// // Static constants (no allocation)
/// const PNG: Extension = Extension::from_static("png", ".png");
/// assert_eq!(PNG.without_period(), "png");
/// assert_eq!(PNG.with_period(), ".png");
///
/// // Runtime parsing (allocates only if needed)
/// let ext = Extension::new("mp4");
/// assert_eq!(ext.without_period(), "mp4");
/// assert_eq!(ext.with_period(), ".mp4");
/// ```
#[derive(Clone, Eq, Hash)]
pub struct Extension {
/// The extension without the leading period, e.g. "png", "mp4".
value: Cow<'static, str>,
/// The extension with the leading period, e.g. ".png", ".mp4".
dotted: Cow<'static, str>,
}
impl Extension {
/// Creates an `Extension` from static strings at compile time.
/// Pass both the bare extension and the dotted form.
///
/// ```
/// use url_utils::extension::extension::Extension;
/// const MP4: Extension = Extension::from_static("mp4", ".mp4");
/// ```
pub const fn from_static(ext: &'static str, dotted: &'static str) -> Self {
Self {
value: Cow::Borrowed(ext),
dotted: Cow::Borrowed(dotted),
}
}
/// Creates an `Extension` from a `&str` at runtime.
/// The input must NOT include a leading period.
pub fn new(ext: &str) -> Self {
let dotted = format!(".{}", ext);
Self {
value: Cow::Owned(ext.to_string()),
dotted: Cow::Owned(dotted),
}
}
/// Returns the extension without the leading period, e.g. `"png"`.
pub fn without_period(&self) -> &str {
&self.value
}
/// Returns the extension with the leading period, e.g. `".png"`.
pub fn with_period(&self) -> &str {
&self.dotted
}
}
impl PartialEq for Extension {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
impl fmt::Debug for Extension {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Extension(\"{}\")", self.value)
}
}
impl fmt::Display for Extension {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.value)
}
}
#[cfg(test)]
mod tests {
use super::*;
const PNG: Extension = Extension::from_static("png", ".png");
const MP4: Extension = Extension::from_static("mp4", ".mp4");
const MP3: Extension = Extension::from_static("mp3", ".mp3");
#[test]
fn static_without_period() {
assert_eq!(PNG.without_period(), "png");
assert_eq!(MP4.without_period(), "mp4");
assert_eq!(MP3.without_period(), "mp3");
}
#[test]
fn static_with_period() {
assert_eq!(PNG.with_period(), ".png");
assert_eq!(MP4.with_period(), ".mp4");
assert_eq!(MP3.with_period(), ".mp3");
}
#[test]
fn runtime_without_period() {
let ext = Extension::new("webm");
assert_eq!(ext.without_period(), "webm");
}
#[test]
fn runtime_with_period() {
let ext = Extension::new("webm");
assert_eq!(ext.with_period(), ".webm");
}
#[test]
fn unknown_extension() {
let ext = Extension::new("foobar");
assert_eq!(ext.without_period(), "foobar");
assert_eq!(ext.with_period(), ".foobar");
}
#[test]
fn equality() {
let a = Extension::new("png");
assert_eq!(a, PNG);
}
#[test]
fn clone() {
let a = Extension::new("wav");
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn display() {
assert_eq!(format!("{}", PNG), "png");
assert_eq!(format!("{}", Extension::new("xyz")), "xyz");
}
#[test]
fn debug() {
assert_eq!(format!("{:?}", PNG), "Extension(\"png\")");
}
}
@@ -0,0 +1,231 @@
use std::collections::HashSet;
use crate::extension::extension::Extension;
use url::Url;
// TODO: Lookups against these as lists will be slow
/// Known image extensions.
const KNOWN_IMAGE_EXTENSIONS: &[Extension] = &[
Extension::from_static("png", ".png"),
Extension::from_static("jpg", ".jpg"),
Extension::from_static("jpeg", ".jpeg"),
Extension::from_static("webp", ".webp"),
];
/// Known audio extensions.
const KNOWN_AUDIO_EXTENSIONS: &[Extension] = &[
Extension::from_static("wav", ".wav"),
Extension::from_static("mp3", ".mp3"),
];
/// Known video extensions.
const KNOWN_VIDEO_EXTENSIONS: &[Extension] = &[
Extension::from_static("mp4", ".mp4"),
Extension::from_static("webm", ".webm"),
];
/// Controls which extensions are accepted when extracting from a URL.
pub enum ExtractExtensions {
/// Accept any extension-like string found in the URL path.
All,
/// Accept only extensions that appear in the given set.
Set(HashSet<Extension>),
/// Accept only known image extensions (png, jpg, jpeg, webp).
KnownImage,
/// Accept only known audio extensions (wav, mp3).
KnownAudio,
/// Accept only known video extensions (mp4, webm).
KnownVideo,
/// Accept any known media extension (image, audio, or video).
KnownMedia,
}
impl ExtractExtensions {
/// Creates a `Set` variant from a `Vec<Extension>`.
pub fn from_vec(extensions: Vec<Extension>) -> Self {
Self::Set(extensions.into_iter().collect())
}
}
/// Extracts a file extension from a URL string, filtered by `accept`.
pub fn extract_extension_from_url_str(url: &str, accept: &ExtractExtensions) -> Option<Extension> {
let parsed_url = Url::parse(url).ok()?;
extract_extension_from_url(&parsed_url, accept)
}
/// Extracts a file extension from a parsed `Url`, filtered by `accept`.
pub fn extract_extension_from_url(url: &Url, accept: &ExtractExtensions) -> Option<Extension> {
let path = url.path();
let raw_ext = std::path::Path::new(path)
.extension()
.and_then(|ext| ext.to_str())?;
let lower = raw_ext.to_lowercase();
let candidate = Extension::new(&lower);
match accept {
ExtractExtensions::All => Some(candidate),
ExtractExtensions::Set(set) => {
if set.contains(&candidate) { Some(candidate) } else { None }
}
ExtractExtensions::KnownImage => {
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate) { Some(candidate) } else { None }
}
ExtractExtensions::KnownAudio => {
if slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate) { Some(candidate) } else { None }
}
ExtractExtensions::KnownVideo => {
if slice_contains(KNOWN_VIDEO_EXTENSIONS, &candidate) { Some(candidate) } else { None }
}
ExtractExtensions::KnownMedia => {
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate)
|| slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate)
|| slice_contains(KNOWN_VIDEO_EXTENSIONS, &candidate)
{
Some(candidate)
} else {
None
}
}
}
}
fn slice_contains(extensions: &[Extension], candidate: &Extension) -> bool {
extensions.iter().any(|e| e == candidate)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_extracts_any_extension() {
let ext = extract_extension_from_url_str("https://example.com/file.png", &ExtractExtensions::All);
assert_eq!(ext.unwrap().without_period(), "png");
}
#[test]
fn all_extracts_unknown_extension() {
let ext = extract_extension_from_url_str("https://example.com/file.foobar", &ExtractExtensions::All);
assert_eq!(ext.unwrap().without_period(), "foobar");
}
#[test]
fn all_extracts_unknown_extension_all_caps() {
let ext = extract_extension_from_url_str("https://example.com/file.FOOBAR", &ExtractExtensions::All);
assert_eq!(ext.unwrap().without_period(), "foobar");
}
#[test]
fn all_lowercases() {
let ext = extract_extension_from_url_str("https://example.com/file.PNG", &ExtractExtensions::All);
assert_eq!(ext.unwrap().without_period(), "png");
}
#[test]
fn known_image_matches_png() {
let ext = extract_extension_from_url_str("https://example.com/photo.png", &ExtractExtensions::KnownImage);
assert_eq!(ext.unwrap().without_period(), "png");
}
#[test]
fn known_image_rejects_mp4() {
let ext = extract_extension_from_url_str("https://example.com/video.mp4", &ExtractExtensions::KnownImage);
assert!(ext.is_none());
}
#[test]
fn known_audio_matches_wav() {
let ext = extract_extension_from_url_str("https://example.com/sound.wav", &ExtractExtensions::KnownAudio);
assert_eq!(ext.unwrap().without_period(), "wav");
}
#[test]
fn known_audio_rejects_png() {
let ext = extract_extension_from_url_str("https://example.com/photo.png", &ExtractExtensions::KnownAudio);
assert!(ext.is_none());
}
#[test]
fn known_video_matches_mp4() {
let ext = extract_extension_from_url_str("https://example.com/clip.mp4", &ExtractExtensions::KnownVideo);
assert_eq!(ext.unwrap().without_period(), "mp4");
}
#[test]
fn known_video_rejects_mp3() {
let ext = extract_extension_from_url_str("https://example.com/song.mp3", &ExtractExtensions::KnownVideo);
assert!(ext.is_none());
}
#[test]
fn known_media_matches_image() {
let ext = extract_extension_from_url_str("https://example.com/photo.jpg", &ExtractExtensions::KnownMedia);
assert_eq!(ext.unwrap().without_period(), "jpg");
}
#[test]
fn known_media_matches_audio() {
let ext = extract_extension_from_url_str("https://example.com/song.mp3", &ExtractExtensions::KnownMedia);
assert_eq!(ext.unwrap().without_period(), "mp3");
}
#[test]
fn known_media_matches_video() {
let ext = extract_extension_from_url_str("https://example.com/clip.webm", &ExtractExtensions::KnownMedia);
assert_eq!(ext.unwrap().without_period(), "webm");
}
#[test]
fn known_media_rejects_unknown() {
let ext = extract_extension_from_url_str("https://example.com/data.xyz", &ExtractExtensions::KnownMedia);
assert!(ext.is_none());
}
#[test]
fn set_matches_custom_list() {
let accept = ExtractExtensions::from_vec(vec![
Extension::from_static("csv", ".csv"),
Extension::from_static("tsv", ".tsv"),
]);
let ext = extract_extension_from_url_str("https://example.com/data.csv", &accept);
assert_eq!(ext.unwrap().without_period(), "csv");
}
#[test]
fn set_rejects_non_member() {
let accept = ExtractExtensions::from_vec(vec![
Extension::from_static("csv", ".csv"),
]);
let ext = extract_extension_from_url_str("https://example.com/data.png", &accept);
assert!(ext.is_none());
}
#[test]
fn no_extension_returns_none() {
let ext = extract_extension_from_url_str("https://example.com/file", &ExtractExtensions::All);
assert!(ext.is_none());
}
#[test]
fn query_params_ignored() {
let ext = extract_extension_from_url_str(
"https://example.com/file.mp4?token=abc&format=hd",
&ExtractExtensions::KnownVideo,
);
assert_eq!(ext.unwrap().without_period(), "mp4");
}
#[test]
fn difficult_url_returns_none() {
let url = "https://videos.youtube.com/foo/bar/files/00000000-f19a-1275-a092-caa60b38f8d2%2Fraw?se=2025-11-21T22%3A26%3A53Z&sp=r&sv=2024-08-09&sr=b";
let ext = extract_extension_from_url_str(url, &ExtractExtensions::All);
assert!(ext.is_none());
}
}
+3 -1
View File
@@ -1,2 +1,4 @@
pub mod download_extension;
pub mod extract_download_extension_from_url;
pub mod extension;
pub mod extract_download_extension_from_url;
pub mod extract_extension_from_url;
@@ -6,6 +6,16 @@ use strum::EnumCount;
use strum::EnumIter;
use utoipa::ToSchema;
/*
Current DB values - this is in conflict with API serializations. Need to fix!
imgref
imgsrc
imgmask
vid_start_frame
vid_end_frame
vidref
*/
/// Used in the `prompt_context_items` table in a `VARCHAR(16)` field.
///
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
@@ -34,6 +44,9 @@ pub enum PromptContextSemanticType {
ImgrefCharacter,
ImgrefStyle,
ImgrefBg,
/// Audio reference (e.g. for audio-to-video generation)
Audioref,
}
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
@@ -54,6 +67,7 @@ impl PromptContextSemanticType {
Self::ImgrefCharacter => "imgref_character",
Self::ImgrefStyle => "imgref_style",
Self::ImgrefBg => "imgref_bg",
Self::Audioref => "audioref",
}
}
@@ -68,6 +82,7 @@ impl PromptContextSemanticType {
"imgref_character" => Ok(Self::ImgrefCharacter),
"imgref_style" => Ok(Self::ImgrefStyle),
"imgref_bg" => Ok(Self::ImgrefBg),
"audioref" => Ok(Self::Audioref),
_ => Err(format!("invalid value: {:?}", value)),
}
}
@@ -85,6 +100,7 @@ impl PromptContextSemanticType {
Self::ImgrefCharacter,
Self::ImgrefStyle,
Self::ImgrefBg,
Self::Audioref,
])
}
}
@@ -108,6 +124,7 @@ mod tests {
assert_serialization(PromptContextSemanticType::ImgrefCharacter, "imgref_character");
assert_serialization(PromptContextSemanticType::ImgrefStyle, "imgref_style");
assert_serialization(PromptContextSemanticType::ImgrefBg, "imgref_bg");
assert_serialization(PromptContextSemanticType::Audioref, "audioref");
}
}
@@ -125,6 +142,7 @@ mod tests {
assert_eq!(PromptContextSemanticType::ImgrefCharacter.to_str(), "imgref_character");
assert_eq!(PromptContextSemanticType::ImgrefStyle.to_str(), "imgref_style");
assert_eq!(PromptContextSemanticType::ImgrefBg.to_str(), "imgref_bg");
assert_eq!(PromptContextSemanticType::Audioref.to_str(), "audioref");
}
#[test]
@@ -138,6 +156,7 @@ mod tests {
assert_eq!(PromptContextSemanticType::from_str("imgref_character").unwrap(), PromptContextSemanticType::ImgrefCharacter);
assert_eq!(PromptContextSemanticType::from_str("imgref_style").unwrap(), PromptContextSemanticType::ImgrefStyle);
assert_eq!(PromptContextSemanticType::from_str("imgref_bg").unwrap(), PromptContextSemanticType::ImgrefBg);
assert_eq!(PromptContextSemanticType::from_str("audioref").unwrap(), PromptContextSemanticType::Audioref);
assert!(PromptContextSemanticType::from_str("foo").is_err());
}
}
@@ -148,7 +167,7 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = PromptContextSemanticType::all_variants();
assert_eq!(variants.len(), 9);
assert_eq!(variants.len(), 10);
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::VidStartFrame));
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::VidEndFrame));
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::VidRef));
@@ -158,6 +177,7 @@ mod tests {
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::ImgrefCharacter));
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::ImgrefStyle));
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::ImgrefBg));
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::Audioref));
assert_eq!(variants.pop_first(), None);
}
}
@@ -79,6 +79,7 @@ thumbnail_generator = { path = "../../../api_clients/thumbnail_generator" }
tokens.workspace = true
tts_common = { path = "../../../lib/tts_common" }
url_config = { path = "../../../lib/url_config" }
url_utils = { path = "../../../lib/url_utils" }
user_input_common = { path = "../../../lib/user_input_common" }
users.workspace = true
videos = { path = "../../../lib/files/videos" }
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::sync::Arc;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
@@ -7,7 +8,7 @@ use crate::http_server::endpoints::generate::common::payments_error_test::paymen
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
use crate::util::lookup::lookup_image_urls_as_map::lookup_image_urls_as_map;
use crate::util::lookup::lookup_media_file_urls_as_map::lookup_media_file_urls_as_map;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
@@ -34,12 +35,15 @@ 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;
use tokens::tokens::media_files::MediaFileToken;
use url::Url;
use url_utils::extension::extract_extension_from_url::{extract_extension_from_url, ExtractExtensions};
/// Seedance 2.0 Multi-Function video generation (text-to-video, keyframe, and reference).
#[utoipa::path(
@@ -91,7 +95,8 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
// --- Collect all media tokens to look up ---
let mut all_media_tokens = Vec::new();
let mut all_media_tokens: Vec<MediaFileToken> = Vec::new();
if let Some(token) = request.start_frame_media_token.as_ref() {
all_media_tokens.push(token.clone());
}
@@ -101,12 +106,18 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
if let Some(tokens) = request.reference_image_media_tokens.as_ref() {
all_media_tokens.extend(tokens.iter().cloned());
}
if let Some(tokens) = request.reference_video_media_tokens.as_ref() {
all_media_tokens.extend(tokens.iter().cloned());
}
if let Some(tokens) = request.reference_audio_media_tokens.as_ref() {
all_media_tokens.extend(tokens.iter().cloned());
}
let image_urls_by_token = if all_media_tokens.is_empty() {
std::collections::HashMap::new()
let file_urls_by_token = if all_media_tokens.is_empty() {
HashMap::new()
} else {
info!("Looking up image media tokens: {:?}", all_media_tokens);
lookup_image_urls_as_map(
info!("Looking up media file tokens: {:?}", all_media_tokens);
lookup_media_file_urls_as_map(
&http_request,
&mut mysql_connection,
server_state.server_environment,
@@ -129,11 +140,11 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
server_state.seedance2pro.cookies.clone()
);
// --- Upload images to seedance2pro CDN ---
// --- Upload files to seedance2pro CDN ---
let start_frame_url = match request.start_frame_media_token.as_ref() {
None => None,
Some(token) => match image_urls_by_token.get(token) {
Some(token) => match file_urls_by_token.get(token) {
None => return Err(CommonWebError::BadInputWithSimpleMessage("Start frame media not found.".to_string())),
Some(url) => Some(upload_to_seedance2pro(&session, url).await?),
}
@@ -141,29 +152,32 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
let end_frame_url = match request.end_frame_media_token.as_ref() {
None => None,
Some(token) => match image_urls_by_token.get(token) {
Some(token) => match file_urls_by_token.get(token) {
None => return Err(CommonWebError::BadInputWithSimpleMessage("End frame media not found.".to_string())),
Some(url) => Some(upload_to_seedance2pro(&session, url).await?),
}
};
let reference_image_urls: Option<Vec<String>> = match request.reference_image_media_tokens.as_ref() {
None => None,
Some(tokens) if tokens.is_empty() => None,
Some(tokens) => {
let mut urls = Vec::with_capacity(tokens.len());
for token in tokens {
match image_urls_by_token.get(token) {
None => return Err(CommonWebError::BadInputWithSimpleMessage("Reference image media not found.".to_string())),
Some(url) => {
let seedance_url = upload_to_seedance2pro(&session, url).await?;
urls.push(seedance_url);
}
}
}
Some(urls)
}
};
let reference_image_urls = upload_reference_tokens_to_seedance2pro(
&session,
&file_urls_by_token,
request.reference_image_media_tokens.as_deref(),
"Reference image",
).await?;
let reference_video_urls = upload_reference_tokens_to_seedance2pro(
&session,
&file_urls_by_token,
request.reference_video_media_tokens.as_deref(),
"Reference video",
).await?;
let reference_audio_urls = upload_reference_tokens_to_seedance2pro(
&session,
&file_urls_by_token,
request.reference_audio_media_tokens.as_deref(),
"Reference audio",
).await?;
// --- Map request params to seedance2pro types ---
@@ -194,6 +208,9 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
start_frame_url,
end_frame_url,
reference_image_urls,
reference_video_urls,
reference_audio_urls,
use_face_blur_hack: None,
};
// --- Calculate cost and charge wallet ---
@@ -277,18 +294,28 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
context_semantic_type: PromptContextSemanticType::VidStartFrame,
});
}
if let Some(media_token) = &request.end_frame_media_token {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidEndFrame,
});
}
if let Some(ref_tokens) = &request.reference_image_media_tokens {
for media_token in ref_tokens {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidRef,
});
let ref_token_groups: [(Option<&[MediaFileToken]>, PromptContextSemanticType); 3] = [
(request.reference_image_media_tokens.as_deref(), PromptContextSemanticType::Imgref),
(request.reference_video_media_tokens.as_deref(), PromptContextSemanticType::VidRef),
(request.reference_audio_media_tokens.as_deref(), PromptContextSemanticType::Audioref),
];
for (maybe_tokens, semantic_type) in ref_token_groups {
if let Some(tokens) = maybe_tokens {
for media_token in tokens {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: semantic_type,
});
}
}
}
@@ -375,11 +402,47 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
}))
}
/// Uploads a list of reference media tokens to seedance2pro, returning the resulting URLs.
/// Returns `None` if the token list is absent or empty.
async fn upload_reference_tokens_to_seedance2pro(
session: &Seedance2ProSession,
file_urls_by_token: &HashMap<MediaFileToken, Url>,
maybe_tokens: Option<&[MediaFileToken]>,
label: &str,
) -> Result<Option<Vec<String>>, CommonWebError> {
let tokens = match maybe_tokens {
None => return Ok(None),
Some(tokens) if tokens.is_empty() => return Ok(None),
Some(tokens) => tokens,
};
let mut urls = Vec::with_capacity(tokens.len());
for token in tokens {
match file_urls_by_token.get(token) {
None => return Err(CommonWebError::BadInputWithSimpleMessage(
format!("{} media not found: {:?}", label, token),
)),
Some(url) => {
let seedance_url = upload_to_seedance2pro(session, url).await?;
urls.push(seedance_url);
}
}
}
Ok(Some(urls))
}
async fn upload_to_seedance2pro(
session: &Seedance2ProSession,
our_cdn_url: &str,
our_cdn_url: &Url,
) -> Result<String, CommonWebError> {
let image_bytes = http_download_url_to_bytes(our_cdn_url)
let extension = extract_extension_from_url(our_cdn_url, &ExtractExtensions::All)
.map(|ext| ext.without_period().to_string())
.unwrap_or_else(|| "png".to_string());
let cdn_url_str = our_cdn_url.as_str();
let file_bytes = http_download_url_to_bytes(cdn_url_str)
.await
.map_err(|err| {
warn!("Error downloading media file from CDN: {:?}", err);
@@ -387,20 +450,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,
})
.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,
})
.await
.map_err(|err| {
warn!("Error uploading file to seedance2pro: {:?}", err);
CommonWebError::ServerError
})?;
@@ -0,0 +1,72 @@
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use actix_web::HttpRequest;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use log::{error, warn};
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::batch_get_media_files_by_tokens_with_connection;
use server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use std::collections::{HashMap, HashSet};
use std::iter::FromIterator;
use tokens::tokens::media_files::MediaFileToken;
use url::Url;
pub async fn lookup_media_file_urls_as_map(
http_request: &HttpRequest,
mysql_connection: &mut PoolConnection<MySql>,
server_environment: ServerEnvironment,
tokens: &[MediaFileToken],
) -> Result<HashMap<MediaFileToken, Url>, CommonWebError> {
const CAN_SEE_DELETED: bool = false;
let result = batch_get_media_files_by_tokens_with_connection(
mysql_connection,
tokens,
CAN_SEE_DELETED,
).await;
let media_files = match result {
Ok(files) => files,
Err(err) => {
error!("Error getting media files by tokens: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
if media_files.len() != tokens.len() {
warn!("Wrong number of media files returned for tokens: {} found for {} tokens", media_files.len(), tokens.len());
let requested : HashSet<&MediaFileToken> = HashSet::from_iter(tokens.iter());
let returned : HashSet<&MediaFileToken> = HashSet::from_iter(media_files.iter().map(|m| &m.token));
let diff = requested.difference(&returned)
.cloned()
.collect::<Vec<&MediaFileToken>>();
return Err(CommonWebError::BadInputWithSimpleMessage(
format!("Not all media files could be found. Media files found: {}, tokens provided: {}, in original: {:?}, req {:?}, ret {:?}",
media_files.len(), tokens.len(), diff, requested, returned)));
}
let media_domain = get_media_domain(&http_request);
let file_urls = media_files.into_iter()
.map(|file| {
let public_bucket_path = MediaFileBucketPath::from_object_hash(
&file.public_bucket_directory_hash,
file.maybe_public_bucket_prefix.as_deref(),
file.maybe_public_bucket_extension.as_deref());
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_environment,
&public_bucket_path);
(file.token, media_links.cdn_url)
})
.collect::<HashMap<_, _>>();
Ok(file_urls)
}
@@ -1,3 +1,4 @@
pub mod fetch_all_required_media_files;
pub mod lookup_image_urls_as_optional_list;
pub mod lookup_image_urls_as_map;
pub mod lookup_image_urls_as_map;
pub mod lookup_media_file_urls_as_map;