Search code examples
c#youtubedownloadbackground-process

Report download progress to user interface


I am working on a project that extracts YouTube videos' audio and saves them to your computer. To do this, I used a library from GitHub called YouTubeExtractor.

I am using a backgroundworker in order to make the UI usable while the file is being downloaded. This is the code I have so far.

public partial class MainWindow : Window
{
    private readonly BackgroundWorker worker = new BackgroundWorker();
    public MainWindow()
    {
        InitializeComponent();
        worker.DoWork += worker_DoWork;
        worker.WorkerReportsProgress = true;
        worker.WorkerSupportsCancellation = true;
    }

    private void downloadButton_Click(object sender, RoutedEventArgs e)
    {
        worker.RunWorkerAsync();
    }
    string link;
    double percentage;
    private void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        this.Dispatcher.Invoke((Action)(() =>
        {
            link = videoURL.Text;
        }));


        /*
         * Get the available video formats.
         * We'll work with them in the video and audio download examples.
         */
        IEnumerable<VideoInfo> videoInfos = DownloadUrlResolver.GetDownloadUrls(link);

        /*
         * We want the first extractable video with the highest audio quality.
         */
        VideoInfo video = videoInfos
            .Where(info => info.CanExtractAudio)
            .OrderByDescending(info => info.AudioBitrate)
            .First();

        /*
         * If the video has a decrypted signature, decipher it
         */
        if (video.RequiresDecryption)
        {
            DownloadUrlResolver.DecryptDownloadUrl(video);
        }

        /*
         * Create the audio downloader.
         * The first argument is the video where the audio should be extracted from.
         * The second argument is the path to save the audio file.
         */
        var audioDownloader = new AudioDownloader(video, System.IO.Path.Combine("C:/Downloads", video.Title + video.AudioExtension));

        // Register the progress events. We treat the download progress as 85% of the progress and the extraction progress only as 15% of the progress,
        // because the download will take much longer than the audio extraction.
        audioDownloader.DownloadProgressChanged += (send, args) => Console.WriteLine(args.ProgressPercentage * 0.85);
        audioDownloader.AudioExtractionProgressChanged += (send, args) => Console.WriteLine(85 + args.ProgressPercentage * 0.15);
        /*
         * Execute the audio downloader.
         * For GUI applications note, that this method runs synchronously.
         */
        audioDownloader.Execute();
    }
}

}

The problem I have is that I want to display this

      audioDownloader.DownloadProgressChanged += (send, args) => Console.WriteLine(args.ProgressPercentage * 0.85);
      audioDownloader.AudioExtractionProgressChanged += (send, args) => Console.WriteLine(85 + args.ProgressPercentage * 0.15);

In a UI element like a label or a progressbar instead of Console.WriteLine

Whenever I do label1.Text = (85 + args.ProgressPercentage * 0.15); It throws me a an error like

" The calling thread cannot access this object because a different thread owns it."

I know you can do solve this with a delegate, I need a clear instruction on how so.

Thank you.


Solution

  • Here's a modern approach for doing this using Tasks and async / await keywords

    Plus the usage of Dispatcher.BeginInvoke for updating your UI.

    enter image description here

    Code:

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Windows;
    using YoutubeExtractor;
    
    namespace WpfApplication1
    {
        public partial class MainWindow
        {
            public MainWindow() {
                InitializeComponent();
            }
    
            private async void Button_Click(object sender, RoutedEventArgs e) {
                string videoUrl = @"https://www.youtube.com/watch?v=5aXsrYI3S6g";
                await DownloadVideoAsync(videoUrl);
            }
    
            private Task DownloadVideoAsync(string url) {
                return Task.Run(() => {
                    IEnumerable<VideoInfo> videoInfos = DownloadUrlResolver.GetDownloadUrls(url);
                    VideoInfo videoInfo = videoInfos.FirstOrDefault();
                    if (videoInfo != null) {
                        if (videoInfo.RequiresDecryption) {
                            DownloadUrlResolver.DecryptDownloadUrl(videoInfo);
                        }
    
                        string savePath =
                            Path.Combine(
                                Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
                                Path.ChangeExtension("myVideo", videoInfo.VideoExtension));
                        var downloader = new VideoDownloader(videoInfo, savePath);
                        downloader.DownloadProgressChanged += downloader_DownloadProgressChanged;
                        downloader.Execute();
                    }
                });
            }
    
            private void downloader_DownloadProgressChanged(object sender, ProgressEventArgs e) {
                Dispatcher.BeginInvoke((Action) (() => {
                    double progressPercentage = e.ProgressPercentage;
                    ProgressBar1.Value = progressPercentage;
                    TextBox1.Text = string.Format("{0:F} %", progressPercentage);
                }));
            }
        }
    }
    

    XAML:

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow"
            Width="525"
            Height="350">
        <Grid>
    
            <StackPanel>
                <Button Click="Button_Click" Content="Download" />
                <ProgressBar x:Name="ProgressBar1"
                             Height="20"
                             Maximum="100" />
                <TextBox x:Name="TextBox1" />
            </StackPanel>
        </Grid>
    </Window>