You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 9+Angular19:youtubeDLSharp下载进度实时回传失败排查

问题分析与修复方案

核心错误

当前代码的关键问题在于Download接口中使用了await Task.Run(...),这会阻塞接口的响应流程——必须等到Task.Run内部的下载任务完全结束,才会返回DownloadId给客户端。客户端无法提前拿到ID进行轮询,自然获取不到中间进度和状态。

修复思路

  1. 取消对后台任务的await,直接启动任务后立即返回DownloadId,让客户端能马上开始轮询状态。
  2. 保证多线程环境下状态读写的线程安全,避免并发访问导致的数据异常。
  3. 优化状态存储和读取逻辑,防止内存泄漏和数据不一致。

修复后的代码示例

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 06:04:56