feat: 实现视频生成完成后自动下载并上传到云端服务
新增功能: 1. 视频生成完成后自动下载到本地临时文件 2. 自动上传到云端S3服务 3. 将S3 URL转换为CDN HTTPS地址 4. 支持防盗链绕过的视频下载 5. 完善的错误处理和fallback机制 技术实现: - download_and_upload_video(): 主要流程控制 - download_video_to_file(): 下载视频到本地临时文件 - 使用CloudUploadService上传到S3 - convert_s3_to_cdn_url(): S3到CDN URL转换 - 临时文件自动清理机制 用户体验: - 视频生成完成后自动获得可访问的CDN链接 - 无需手动下载和上传操作 - 支持原始URL作为fallback保证可用性 - 详细的日志记录便于问题排查 安全特性: - 防盗链绕过HTTP头设置 - 临时文件安全清理 - 错误情况下的资源释放 - 超时控制防止长时间阻塞
This commit is contained in:
parent
d4b9e77020
commit
6c43e6f08b
|
|
@ -325,10 +325,22 @@ impl VolcanoVideoService {
|
|||
match data.status.as_str() {
|
||||
"done" => {
|
||||
if let Some(video_url) = data.video_url {
|
||||
record.mark_as_completed(video_url, None); // 火山云API没有缩略图
|
||||
self.repository.update(&record).await?;
|
||||
info!("视频生成任务完成: {}", record_id);
|
||||
return Ok(());
|
||||
// 下载视频并上传到云端
|
||||
match self.download_and_upload_video(&video_url, &record_id).await {
|
||||
Ok(cdn_url) => {
|
||||
record.mark_as_completed(cdn_url, None); // 使用CDN URL
|
||||
self.repository.update(&record).await?;
|
||||
info!("视频生成任务完成,已上传到CDN: {}", record_id);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("视频上传到CDN失败,使用原始URL: {} - {}", record_id, e);
|
||||
record.mark_as_completed(video_url, None); // 使用原始URL作为fallback
|
||||
self.repository.update(&record).await?;
|
||||
info!("视频生成任务完成(使用原始URL): {}", record_id);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
record.mark_as_failed("视频URL为空".to_string(), Some("EMPTY_VIDEO_URL".to_string()));
|
||||
self.repository.update(&record).await?;
|
||||
|
|
@ -594,6 +606,100 @@ impl VolcanoVideoService {
|
|||
Ok(hex::encode(signature))
|
||||
}
|
||||
|
||||
/// 下载视频并上传到云端服务
|
||||
async fn download_and_upload_video(&self, video_url: &str, record_id: &str) -> Result<String> {
|
||||
info!("开始下载并上传视频: {} -> {}", video_url, record_id);
|
||||
|
||||
// 创建临时文件路径
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let temp_filename = format!("volcano_video_{}_{}.mp4", record_id, chrono::Utc::now().timestamp());
|
||||
let temp_file_path = temp_dir.join(&temp_filename);
|
||||
let temp_file_str = temp_file_path.to_string_lossy().to_string();
|
||||
|
||||
// 下载视频到本地临时文件
|
||||
info!("正在下载视频到临时文件: {}", temp_file_str);
|
||||
match self.download_video_to_file(video_url, &temp_file_str).await {
|
||||
Ok(_) => {
|
||||
info!("视频下载成功: {}", temp_file_str);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("视频下载失败: {} - {}", video_url, e);
|
||||
return Err(anyhow!("视频下载失败: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 上传到云端服务
|
||||
info!("正在上传视频到云端: {}", temp_file_str);
|
||||
let upload_result = match self.cloud_upload_service.upload_file(&temp_file_str, None, None).await {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
// 清理临时文件
|
||||
let _ = tokio::fs::remove_file(&temp_file_path).await;
|
||||
error!("视频上传失败: {} - {}", temp_file_str, e);
|
||||
return Err(anyhow!("视频上传失败: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
// 清理临时文件
|
||||
if let Err(e) = tokio::fs::remove_file(&temp_file_path).await {
|
||||
warn!("清理临时文件失败: {} - {}", temp_file_str, e);
|
||||
}
|
||||
|
||||
if !upload_result.success {
|
||||
let error_msg = upload_result.error_message.unwrap_or_default();
|
||||
error!("视频上传失败: {}", error_msg);
|
||||
return Err(anyhow!("视频上传失败: {}", error_msg));
|
||||
}
|
||||
|
||||
let s3_url = upload_result.remote_url
|
||||
.ok_or_else(|| anyhow!("上传成功但未返回S3 URL"))?;
|
||||
|
||||
// 转换S3 URL为CDN URL
|
||||
let cdn_url = Self::convert_s3_to_cdn_url(&s3_url);
|
||||
info!("视频上传成功: {} -> {} -> {}", video_url, s3_url, cdn_url);
|
||||
|
||||
Ok(cdn_url)
|
||||
}
|
||||
|
||||
/// 下载视频文件到本地
|
||||
async fn download_video_to_file(&self, video_url: &str, file_path: &str) -> Result<()> {
|
||||
// 创建带有防盗链绕过的HTTP客户端
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||
.timeout(std::time::Duration::from_secs(300)) // 5分钟超时
|
||||
.build()
|
||||
.map_err(|e| anyhow!("创建HTTP客户端失败: {}", e))?;
|
||||
|
||||
// 构建请求,添加必要的头部信息绕过防盗链
|
||||
let request = client
|
||||
.get(video_url)
|
||||
.header("Referer", "https://www.volcengine.com/")
|
||||
.header("Origin", "https://www.volcengine.com")
|
||||
.header("Accept", "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5")
|
||||
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
.header("Cache-Control", "no-cache")
|
||||
.header("Pragma", "no-cache")
|
||||
.header("Sec-Fetch-Dest", "video")
|
||||
.header("Sec-Fetch-Mode", "no-cors")
|
||||
.header("Sec-Fetch-Site", "cross-site");
|
||||
|
||||
let response = request.send().await
|
||||
.map_err(|e| anyhow!("请求视频失败: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!("下载视频失败,HTTP状态码: {}", response.status()));
|
||||
}
|
||||
|
||||
let bytes = response.bytes().await
|
||||
.map_err(|e| anyhow!("读取视频数据失败: {}", e))?;
|
||||
|
||||
tokio::fs::write(file_path, &bytes).await
|
||||
.map_err(|e| anyhow!("保存视频文件失败: {}", e))?;
|
||||
|
||||
info!("视频文件保存成功: {} ({} bytes)", file_path, bytes.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将S3 URL转换为可访问的CDN地址
|
||||
fn convert_s3_to_cdn_url(s3_url: &str) -> String {
|
||||
if s3_url.starts_with("s3://ap-northeast-2/modal-media-cache/") {
|
||||
|
|
|
|||
Loading…
Reference in New Issue