From 6c43e6f08b96b6496630c92df0ee7b5d74f5d4b3 Mon Sep 17 00:00:00 2001 From: imeepos Date: Thu, 31 Jul 2025 14:03:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=AE=8C=E6=88=90=E5=90=8E=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=B9=B6=E4=B8=8A=E4=BC=A0=E5=88=B0=E4=BA=91?= =?UTF-8?q?=E7=AB=AF=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: 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头设置 - 临时文件安全清理 - 错误情况下的资源释放 - 超时控制防止长时间阻塞 --- .../services/volcano_video_service.rs | 114 +++++++++++++++++- 1 file changed, 110 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src-tauri/src/business/services/volcano_video_service.rs b/apps/desktop/src-tauri/src/business/services/volcano_video_service.rs index d344ac9..6cdb321 100644 --- a/apps/desktop/src-tauri/src/business/services/volcano_video_service.rs +++ b/apps/desktop/src-tauri/src/business/services/volcano_video_service.rs @@ -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 { + 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/") {