using Downloader; using Microsoft.Extensions.DependencyInjection; using SqlSugar; using SqlSugar.IOC; using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Net; using System.Threading.Tasks; using VideoAnalysisCore.Job; using VideoAnalysisCore.Model; using VideoAnalysisCore.Model.Enum; using AlibabaCloud.SDK.Vod20170321; using UserCenter.Model.Enum; using System.Security.Policy; using AlibabaCloud.SDK.Vod20170321.Models; namespace VideoAnalysisCore.Common { public static class DownloadFileExpand { /// /// 初始化下载器 /// /// 下载速度mb/s public static void AddDownloadFileExpand(this IServiceCollection services) { DownloadFile.DownloadSpeed = AppCommon.Config.TaskSetting.DownloadSpeed; services.AddTransient(); DownloadFile.Opt = new DownloadConfiguration() { // 通常,主机支持的最大值为8000字节,默认值是8000 BufferBlockSize = 10240, // 要下载的文件部分数量,默认值是1 ChunkCount = 8, // 下载速度限制为2MB/秒,默认值为零(即无限制) MaximumBytesPerSecond = 1024 * 1024 * DownloadFile.DownloadSpeed, // 故障转移时的最大重试次数 MaxTryAgainOnFailover = 5, // 每50MB后释放内存缓冲区 MaximumMemoryBufferBytes = 1024 * 1024 * 50, // 是否并行下载文件的各个部分。默认值为false ParallelDownload = true, // 并行下载的数量。默认值与分块数量相同 ParallelCount = 4, // 每个流块读取器的超时时间(毫秒),默认值是1000 Timeout = 1000, // 如果只想下载大型文件的特定字节范围,则设置为true RangeDownload = false, // 大型文件下载范围的起始偏移量 RangeLow = 0, // 大型文件下载范围的结束偏移量 RangeHigh = 0, // 下载失败完成时清除包块数据,默认值为false ClearPackageOnCompletionWithFailure = true, // 将文件分多个部分下载时的最小分块大小,默认值是512 MinimumSizeOfChunking = 1024, // 在开始下载之前,按照文件大小预留文件的存储空间,默认值为false ReserveStorageSpaceBeforeStartingDownload = true, // 在下载进度改变事件中,通过ReceivedBytes获取按需下载的数据 EnableLiveStreaming = false, // 配置和自定义请求头 RequestConfiguration = { Accept = "*/*", //CookieContainer = cookies, Headers = new WebHeaderCollection(), // { 你的自定义头部信息 } KeepAlive = true, // 默认值为false ProtocolVersion = HttpVersion.Version11, // 默认值是HTTP 1.1 UseDefaultCredentials = false, // 你的自定义用户代理或你的应用名称/应用版本。 UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", //Proxy = new WebProxy()//使用代理 //{ // Address = new Uri("http://Learn.VideoAnalysis"), // UseDefaultCredentials = false, // Credentials = System.Net.CredentialCache.DefaultNetworkCredentials, // BypassProxyOnLocal = true //} } }; } } /// /// /// public class DownloadFile { public static DownloadConfiguration Opt { get; set; } = default!; public static int DownloadSpeed { get; set; } = default!; private readonly Repository videoTaskDB; private readonly Repository packageInfoTaskDB; private readonly Client vodClient; private readonly RedisManager redisManager; private readonly IServiceProvider serviceProvider; readonly string taskVideoName = "task.mp4"; readonly string taskPPTVideoName = "ppt.mp4"; // 注入工作流管理器,用于更新进度 private readonly VideoSliceWorkflowManager _videoSliceWorkflowManager; private readonly TidySlideWorkflowManager _tidySlideWorkflowManager; public DownloadFile(Repository videoTaskDB, Client vodClient, Repository nackageInfoTaskDB, RedisManager redisManager, IServiceProvider serviceProvider, VideoSliceWorkflowManager videoSliceWorkflowManager, TidySlideWorkflowManager tidySlideWorkflowManager) { this.videoTaskDB = videoTaskDB; this.vodClient = vodClient; this.packageInfoTaskDB = nackageInfoTaskDB; this.redisManager = redisManager; this.serviceProvider = serviceProvider; _videoSliceWorkflowManager = videoSliceWorkflowManager; _tidySlideWorkflowManager = tidySlideWorkflowManager; } // 辅助方法:根据工作流名称获取对应的管理器实例 private dynamic GetWorkflowManager(string workflowName) { if (workflowName == "TidySlideWorkflow") return _tidySlideWorkflowManager; return _videoSliceWorkflowManager; } // 根据 Content-Type 映射文件后缀 static string GetExtensionFromContentType(HttpResponseMessage res) { var contentType = res.Content.Headers.ContentType?.MediaType; return contentType switch { "application/pdf" => ".pdf", "image/jpeg" => ".jpg", "image/png" => ".png", "application/zip" => ".zip", "text/html" => ".html", "audio/wav" => ".wav", "audio/mp4" => ".mp4", "audio/mp3" => ".mp3", // 根据需要添加其他 Content-Type 映射 _ => ".mp4", // 默认二进制文件 }; } /// /// 使用 HttpClient 下载任务的资源文件到本地 /// /// /// public async Task RunTask(string task, string workflowName = "VideoSliceWorkflow") { var taskId = long.Parse(task); var taskInfo = await GetTaskInfoAsync(taskId); var fileUrl = await GetMediaUrlAsync(taskInfo); // 准备本地目录 var localPath = task.LocalPath(); await PrepareDirectoryAndDbAsync(taskId, localPath); // 处理 PPT 视频 await ProcessPPTVideoAsync(taskInfo, localPath, taskId, workflowName, task); // 下载主视频 await DownloadWithCacheCheckAsync(taskId, fileUrl, localPath, taskVideoName, workflowName, task, "主视频"); } private async Task GetTaskInfoAsync(long taskId) { var taskInfo = await videoTaskDB.CopyNew().AsQueryable() .Where(s => s.Id == taskId).FirstAsync(); if (taskInfo is null) throw new Exception($"任务为null/是教研视频/没有视频课程名称"); return taskInfo; } private async Task GetMediaUrlAsync(VideoTask taskInfo) { var fileUrl = taskInfo.MediaUrl; if (string.IsNullOrEmpty(fileUrl)) { var videoInfo = await vodClient.GetPlayInfoAsync(new GetPlayInfoRequest() { VideoId = taskInfo.TagId, Formats = "mp4", OutputType = "cdn", AuthTimeout = 3600 * 24 * 12, }); if (videoInfo is null || videoInfo.StatusCode != 200 && !videoInfo.Body.PlayInfoList.PlayInfo.Any()) throw new Exception($"{DateTime.Now} 视频订阅=>获取阿里云视频信息失败 VideoCode {taskInfo.TagId} StatusCode {videoInfo?.StatusCode}"); fileUrl = videoInfo.Body.PlayInfoList.PlayInfo.First().PlayURL; } if (string.IsNullOrEmpty(fileUrl)) throw new Exception($"任务id[{taskInfo.Id}] 资源地址无效 {fileUrl}"); // 尝试从 URL 中获取文件后缀 (保留原有校验逻辑) string fileExtension = Path.GetExtension(new Uri(fileUrl).AbsolutePath); if (string.IsNullOrEmpty(fileExtension)) throw new Exception($"未能从资源路径中获取文件后缀"); return fileUrl; } private async Task PrepareDirectoryAndDbAsync(long taskId, string localPath) { if (!Directory.Exists(AppCommon.TaskCachedFile)) Directory.CreateDirectory(AppCommon.TaskCachedFile); if (!Directory.Exists(localPath)) Directory.CreateDirectory(localPath); var outputPath = Path.Combine(localPath, taskVideoName); await videoTaskDB.CopyNew() .AsUpdateable() .SetColumns(it => it.LocalMediaPath == outputPath) .Where(it => it.Id == taskId) .ExecuteCommandAsync(); } private async Task ProcessPPTVideoAsync(VideoTask taskInfo, string localPath, long taskId, string workflowName, string taskStr) { // 更新 PPT 视频 URL if (string.IsNullOrEmpty(taskInfo.PPTVideoUrl) && !string.IsNullOrEmpty(taskInfo.PPTVideoCode)) { taskInfo.PPTVideoUrl = await packageInfoTaskDB.AsQueryable() .Where(s => s.VideoCode == taskInfo.PPTVideoCode) .Select(s => s.VideoUrl) .FirstAsync(); if (!string.IsNullOrEmpty(taskInfo.PPTVideoUrl)) { await videoTaskDB.CopyNew().AsUpdateable(taskInfo) .UpdateColumns(it => new { it.PPTVideoUrl }) .ExecuteCommandAsync(); } } // 下载 PPT 视频 if (!string.IsNullOrEmpty(taskInfo.PPTVideoUrl)) { await DownloadWithCacheCheckAsync(taskId, taskInfo.PPTVideoUrl, localPath, taskPPTVideoName, workflowName, taskStr, "PPT视频", true); } } private async Task DownloadWithCacheCheckAsync(long taskId, string url, string localPath, string fileName, string workflowName, string taskStr, string logPrefix, bool isPPT = false) { var filePath = Path.Combine(localPath, fileName); if (File.Exists(filePath) && new FileInfo(filePath).Length > 10 * 1024 * 1024) { await GetWorkflowManager(workflowName).AddTaskLog(taskId, $"{logPrefix}命中本地缓存"); } else { await Download(url, localPath, fileName, (s, e) => { var progressPrefix = isPPT ? "PPT->" : ""; GetWorkflowManager(workflowName).SetTaskProgress(taskStr, $"{progressPrefix}" + Math.Round(e.ProgressPercentage, 1)); }); } } /// /// 下载文件 /// /// /// /// /// public async Task Download(string fileUrl, string localPath, string fileName,Action change) { var res = new TaskCompletionSource(); using IDownload download = DownloadBuilder.New() .WithUrl(fileUrl) .WithDirectory(localPath) .WithFileName(fileName) .WithConfiguration(Opt) .Build(); var pI = 0; download.DownloadProgressChanged += (object? sender, Downloader.DownloadProgressChangedEventArgs e) => { pI++; if (pI % 20 == 0) change(sender, e); }; download.DownloadFileCompleted += async (object? sender, AsyncCompletedEventArgs e) => { if (download.Status == DownloadStatus.Failed && e.Error != null) { // 检查磁盘空间不足异常 if (e.Error.Message.Contains("not enough space on the disk", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($"{DateTime.Now} 下载失败:磁盘空间不足。尝试清理缓存..."); try { using var scope = serviceProvider.CreateScope(); var clearJob = scope.ServiceProvider.GetRequiredService(); await clearJob.Invoke(); } catch (Exception ex) { Console.WriteLine($"清理缓存失败: {ex.Message}"); } res.SetException(new Exception($"磁盘空间不足,下载失败。请手动清理盘符 {Path.GetPathRoot(localPath)}。详细错误:{e.Error.Message}")); } else { res.SetException(e.Error); } } else if (download.Status == DownloadStatus.Completed) { res.SetResult(); } }; await download.StartAsync(); // 等待回调函数完成 await res.Task; } } }