.NET 9+Angular19:youtubeDLSharp下载进度实时回传失败排查
问题分析与修复方案
核心错误
当前代码的关键问题在于Download接口中使用了await Task.Run(...),这会阻塞接口的响应流程——必须等到Task.Run内部的下载任务完全结束,才会返回DownloadId给客户端。客户端无法提前拿到ID进行轮询,自然获取不到中间进度和状态。
修复思路
- 取消对后台任务的
await,直接启动任务后立即返回DownloadId,让客户端能马上开始轮询状态。 - 保证多线程环境下状态读写的线程安全,避免并发访问导致的数据异常。
- 优化状态存储和读取逻辑,防止内存泄漏和数据不一致。
修复后的代码示例
C#控制器代码
// 注意:将原有的Dictionary替换为ConcurrentDictionary保证线程安全 private readonly ConcurrentDictionary<string, DownloadStatus> _downloads = new(); [HttpPost("download")] public IActionResult Download([FromBody] DownloadRequest request) { if (string.IsNullOrEmpty(request.Url)) return BadRequest("URL is required"); string downloadId = Guid.NewGuid().ToString(); var downloadStatus = new DownloadStatus { Id = downloadId, Url = request.Url, State = "Initializing", Progress = 0, IsCompleted = false, Output = new List<string>() // 提前初始化避免空引用 }; // 线程安全地存储初始状态 _downloads.TryAdd(downloadId, downloadStatus); // 启动后台任务,不等待完成,立即返回ID _ = Task.Run(async () => { try { var progress = new Progress<DownloadProgress>(p => { // 加锁保证状态更新的原子性 lock (downloadStatus) { downloadStatus.State = p.State.ToString(); downloadStatus.Progress = p.Progress; downloadStatus.DownloadSpeed = p.DownloadSpeed; downloadStatus.ETA = p.ETA; } }); var output = new Progress<string>(s => { lock (downloadStatus) { downloadStatus.Output.Add(s); } }); OptionSet? custom = null; if (!string.IsNullOrEmpty(request.Options)) { custom = OptionSet.FromString(request.Options.Split('\n')); } RunResult<string> result = request.AudioOnly ? await _youtubeDL.RunAudioDownload(request.Url, AudioConversionFormat.Mp3, progress: progress, output: output, overrideOptions: custom) : await _youtubeDL.RunVideoDownload(request.Url, progress: progress, output: output, overrideOptions: custom); lock (downloadStatus) { downloadStatus.IsCompleted = true; downloadStatus.IsSuccessful = result.Success; downloadStatus.FilePath = result.Success ? result.Data : null; downloadStatus.ErrorMessage = result.Success ? null : string.Join("\n", result.ErrorOutput); } } catch (Exception ex) { lock (downloadStatus) { downloadStatus.IsCompleted = true; downloadStatus.IsSuccessful = false; downloadStatus.ErrorMessage = ex.Message; } } finally { // 下载完成10分钟后清理状态,避免内存泄漏 _ = Task.Delay(TimeSpan.FromMinutes(10)).ContinueWith(_ => _downloads.TryRemove(downloadId, out _)); } }); // 立即返回下载ID,客户端可开始轮询 return Ok(new { DownloadId = downloadId }); } [HttpGet("status/{id}")] public IActionResult GetDownloadStatus(string id) { if (!_downloads.TryGetValue(id, out var status)) { return NotFound("Download not found"); } // 加锁读取完整状态,并返回副本避免客户端干扰服务器数据 lock (status) { var statusCopy = new DownloadStatus { Id = status.Id, Url = status.Url, State = status.State, Progress = status.Progress, DownloadSpeed = status.DownloadSpeed, ETA = status.ETA, Output = new List<string>(status.Output), IsCompleted = status.IsCompleted, IsSuccessful = status.IsSuccessful, FilePath = status.FilePath, ErrorMessage = status.ErrorMessage }; return Ok(statusCopy); } }
关键修改说明
- 移除
await Task.Run(...)中的await,改用_ = Task.Run(...)启动后台任务,接口立即响应返回DownloadId。 - 将
_downloads替换为ConcurrentDictionary,保证多线程下的状态存储安全。 - 对
downloadStatus的读写操作添加lock,防止并发访问导致的状态不一致。 - 在
GetDownloadStatus中返回状态副本,避免客户端直接修改服务器端对象。 - 添加状态自动清理逻辑,防止内存无限增长。
额外注意事项
IIS默认会回收闲置进程,长时间运行的下载任务可能被中断。如果需要稳定的后台任务处理,建议使用IHostedService或第三方任务调度库(如Hangfire)替代简单的Task.Run。
内容的提问来源于stack exchange,提问作者D052057
相关产品推荐
相关产品推荐

