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

WPF转WinForms版YouTube下载器出现HTML错误,求排查

WPF转Windows Forms YouTube下载器HTML错误排查

我从GitHub获取了一款可正常运行的WPF版YouTube下载器C#代码,将其改写为Windows Forms版本后,暂时注释了播放列表下载功能,仅保留单视频下载,但运行时出现HTML错误,需要排查方法。

原WPF版核心代码

BeginDownload方法

public async Task BeginDownload()
{
    if (!CheckInputs())
        return;

    ChangeButtonStates(false);

    string link = videoLink.Text;
    string? downloadFormat = null;
    string savedDirectory = Properties.Settings.Default.savedDirectory;

    if (saveMP3.IsChecked == true)
        downloadFormat = "mp3";
    else
        downloadFormat = "mp4";

    try
    {
        cancellationToken = cancellationTokenSource.Token;

        // If the given URL contains "&list" it means it is playlist
        if (!link.Contains("&list"))
            await DownloadSingle(link, savedDirectory, downloadFormat);
        else
            await DownloadPlaylist(link, savedDirectory, downloadFormat);
    }
    catch (Exception ex)
    {
        new Thread(() =>
        {
            MessageBox.Show($"An error occurred: \"{ex.Message}\"", "Downloader", MessageBoxButton.OK, MessageBoxImage.Error);

        }).Start();
    }

    ChangeButtonStates(true);
}

单视频与播放列表下载方法

public async Task DownloadSingle(string link, string path, string format)
{
    // Needed for security
    var handler = new HttpClientHandler();
    var httpClient = new HttpClient(handler, true);
    handler.UseCookies = false;

    // Get video data
    var youtube = new YoutubeClient(httpClient);
    var streamData = await youtube.Videos.GetAsync(link);
    var title = ReplaceInvalidCharacters(streamData.Title);

    var progress = new Progress<double>(value =>
    {
            // To split the progress bar into two halves, fill one half and then the next,
            // maximum of both progress bars is 50
            if (downloadProgressOne.Value != 50)
        {
            downloadProgressOne.Value = value * 100.00f;
        }
        else
        {
            downloadProgressTwo.Value = (value * 100.00f) - 50;
        }

            // Taskbar icon progress bar
            taskbarIcon.ProgressValue = value;

        downloadStatus.Text = $"Downloading... {Convert.ToInt32(value * 100.00f)}%";
    });

    try
    {
        // Download content
        await youtube.Videos.DownloadAsync(link, $"{path}\\{title}.{format}", o => o.SetContainer(format).SetPreset(ConversionPreset.UltraFast), progress, cancellationToken);
    }
    catch (TaskCanceledException)
    {
        new Thread(() =>
        {
            MessageBox.Show($"Successfully cancelled the download of: \"{title}\"", "Downloader", MessageBoxButton.OK, MessageBoxImage.Information);

        }).Start();

        File.Delete($"{path}\\{title}.{format}");
        return;
    }
    catch (Exception ex)
    {
        new Thread(() =>
        {
            MessageBox.Show($"Failed to download video: \"{title}\" due to an error.\n\nReason: \"{ex.Message}\"", "Downloader", MessageBoxButton.OK, MessageBoxImage.Warning);

        }).Start();

        return;
    }

    new Thread(() =>
    {
        MessageBox.Show($"Successfully downloaded video: \"{title}\"", "Downloader", MessageBoxButton.OK, MessageBoxImage.Information);

    }).Start();
}

public async Task DownloadPlaylist(string link, string path, string format)
{
    // Create a string list incase any videos fail to download
    List<string> failedVideosTitles = new();
    string finalList = "";
    int failedVideosAmount = 0;

    // Needed for security
    var handler = new HttpClientHandler();
    var httpClient = new HttpClient(handler, true);
    handler.UseCookies = false;

    // Get playlist data
    var youtube = new YoutubeClient(httpClient);
    var playlistData = await youtube.Playlists.GetAsync(link);
    var playlistName = ReplaceInvalidCharacters(playlistData.Title);
    var total = await youtube.Playlists.GetVideosAsync(link);
    var totalNumber = total.Count;

    int currentNumber = 0;

    // Foreach video in the playlist try to download them as the desired format
    await foreach (var video in youtube.Playlists.GetVideosAsync(playlistData.Id))
    {
        currentNumber++;
        var title = ReplaceInvalidCharacters(video.Title);

        // Skip download of video if it already exists
        if (File.Exists($"{path}\\{title}.{format}"))
        {
            downloadStatus.Text = $"Skipping {currentNumber}/{totalNumber}...";
            await Task.Delay(100);
            continue;
        }

        var progress = new Progress<double>(value =>
        {
            // To split the progress bar into two halves, fill one half and then the next,
            // maximum of both progress bars is 50
            if (value < 0.5f || downloadProgressOne.Value < 50)
            {
                downloadProgressOne.Value = value * 100.00f;
                downloadProgressTwo.Value = 0;
            }
            else
                downloadProgressTwo.Value = (value * 100.00f) - 50;

            // Taskbar icon progress bar
            taskbarIcon.ProgressValue = value;

            downloadStatus.Text = $"Downloading... {currentNumber}/{totalNumber} - {Convert.ToInt32(value * 100.00f)}%";
        });

        try
        {
            // Download content
            await youtube.Videos.DownloadAsync(video.Id, $"{path}\\{title}.{format}", o => o.SetContainer(format).SetPreset(ConversionPreset.UltraFast), progress, cancellationToken);
        }
        catch (TaskCanceledException)
        {
            new Thread(() =>
            {
                MessageBox.Show($"Successfully cancelled the download of playlist: \"{playlistName}\"\n\nFiles have not been deleted.", "Downloader", MessageBoxButton.OK, MessageBoxImage.Information);

            }).Start();

            File.Delete($"{path}\\{title}.{format}");
            return;
        }
        catch (Exception ex)
        {
            new Thread(() =>
            {
                // Increase the failed videos amount by one and add the title to the list
                failedVideosAmount++;
                failedVideosTitles.Add($"\"{title}\"");

                MessageBox.Show($"Skipping download of video: \"{title}\" due to an error.\n\nReason: \"{ex.Message}\"", "Downloader", MessageBoxButton.OK, MessageBoxImage.Warning);

            }).Start();
        }
    }

    if (failedVideosAmount != 0)
    {
        new Thread(() =>
        {
            // Show a messagebox telling the user it failed to download X amount of videos
            MessageBox.Show($"Downloaded playlist: \"{playlistName}\" but failed to download {failedVideosAmount} of the videos.\n\nPress OK to see list of failed videos.", "Downloader", MessageBoxButton.OK, MessageBoxImage.Warning);

            // Loop for the length of the string list, build a final string containing
            // a list of titles of failed videos then display it in a messagebox for the user
            for (int i = 0; i < failedVideosTitles.Count; i++)
            {
                if (i == 0) { finalList = $"{finalList}{i + 1}. {failedVideosTitles[i]}."; }
                else { finalList = $"{finalList}\n\n{i + 1}. {failedVideosTitles[i]}."; }
            }

            MessageBox.Show(finalList, "Downloader", MessageBoxButton.OK, MessageBoxImage.Information);

        }).Start();
    }
    else
    {
        new Thread(() =>
        {
            // The entire playlist was downloaded successfully
            MessageBox.Show($"Successfully downloaded playlist: \"{playlistName}\"", "Downloader", MessageBoxButton.OK, MessageBoxImage.Information);

        }).Start();
    }
}

转换后的Windows Forms代码

BeginDownload方法

public async Task BeginDownload()
{
    if (!CheckInputs())
        return;

    ChangeButtonStates(false);

    string link = videoLink.Text;
    string downloadFormat = null;
    string savedDirectory = Properties.Settings.Default.savedDirectory;

    if (cmbDownloadType.SelectedIndex == 1)
        downloadFormat = "mp3";
    else
        downloadFormat = "mp4";

    try
    {
        cancellationToken = cancellationTokenSource.Token;

        //// If the given URL contains "&list" it means it is playlist
        //if (!link.Contains("&list"))
        await DownloadSingle(link, savedDirectory, downloadFormat);
        //else
        //await DownloadPlaylist(link, savedDirectory, downloadFormat);
    }
    catch (Exception ex)
    {
        new Thread(() =>
        {
            MessageBox.Show($"An error occurred: \"{ex.Message}\"", "Downloader", MessageBoxButtons.OK, MessageBoxIcon.Error);

        }).Start();
    }

    ChangeButtonStates(true);
}

单视频下载方法

public async Task DownloadSingle(string link, string path, string format)
{
    // Needed for security
    var handler = new HttpClientHandler();
    var httpClient = new HttpClient(handler, true);
    handler.UseCookies = false;

    // Get video data
    var youtube = new YoutubeClient(httpClient);
    var streamData = await youtube.Videos.GetAsync(link);
    var title = ReplaceInvalidCharacters(streamData.Title);

    var progress = new Progress<double>(value =>
    {
        // To split the progress bar into two halves, fill one half and then the next,
        // maximum of both progress bars is 50
        if (downloadProgressOne.Value != 50)
        {
            downloadProgressOne.Value = (int)(value * 100.00f);
        }
        else
        {
            downloadProgressTwo.Value = (int)((value * 100.00f) - 50);
        }

        // Taskbar icon progress bar
        //taskbarIcon.ProgressValue = value;

        downloadStatus.Text = $"Downloading... {Convert.ToInt32(value * 100.00f)}%";
    });

    try
    {
        // Download content
        await youtube.Videos.DownloadAsync(link, $"{path}\\{title}.{format}", o => o.SetContainer(format).SetPreset(ConversionPreset.UltraFast), progress, cancellationToken);
    }
    catch (TaskCanceledException)
    {
        new Thread(() =>
        {
            MessageBox.Show($"Successfully cancelled the download of: \"{title}\"", "Downloader", MessageBoxButtons.OK, MessageBoxIcon.Information);

        }).Start();

        File.Delete($"{path}\\{title}.{format}");
        return;
    }
    catch (Exception ex)
    {
        new Thread(() =>
        {
            MessageBox.Show($"Failed to download video: \"{title}\" due to an error.\n\nReason: \"{ex.Message}\"", "Downloader", MessageBoxButtons.OK, MessageBoxIcon.Warning);

        }).Start();

        return;
    }

    new Thread(() =>
    {
        MessageBox.Show($"Successfully downloaded video: \"{title}\"", "Downloader", MessageBoxButtons.OK, MessageBoxIcon.Information);

    }).Start();
}

排查建议

  • 获取完整异常信息:在所有catch块中,将ex.Message替换为ex.ToString(),捕获完整的异常堆栈跟踪、内部异常等细节,准确定位HTML错误的触发位置(比如是GetAsync还是DownloadAsync阶段抛出的)。
  • 修复UI线程访问问题:WinForms控件只能在创建它们的UI线程上操作,弹出MessageBox的新线程写法可能引发跨线程问题。建议改用Invoke方法在UI线程显示弹窗,例如:
    this.Invoke((MethodInvoker)delegate {
        MessageBox.Show($"Failed to download video: \"{title}\" due to an error.\n\nReason: \"{ex.ToString()}\"", "Downloader", MessageBoxButtons.OK, MessageBoxIcon.Warning);
    });
    
  • 验证依赖库版本:确认WinForms项目中使用的YouTube库(如YoutubeExplode)版本与原WPF项目完全一致,不同版本的库可能对YouTube页面HTML结构的解析逻辑不同,导致错误。
  • 检查输入链接有效性:测试多个不同格式的YouTube视频链接(如https://www.youtube.com/watch?v=XXX、https://youtu.be/XXX),排除因链接格式异常导致的解析错误。
  • 调试网络请求结果:在YoutubeClient.Videos.GetAsync(link)调用后,添加调试日志,查看返回的streamData是否包含有效数据,或者是否返回了反爬验证页面的HTML内容。
  • 确认目录权限:检查保存目录path是否存在且有写入权限,避免因文件写入失败引发的间接异常被误判为HTML错误。

内容的提问来源于stack exchange,提问作者user2180706

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 17:05:28