-2

Before you all go on a rampage about how this is a duplicate question, I have spent two days working on this issue, watching youtube tutorials on asynchronous programming, surfing similar stackoverflow posts etc, and I cannot for the life of me figure out how to apply Asynchronous Parallel Downloading of files into my project.

First things first, some background:

I am creating a program that, when given a query input via the user, will make a call to the twitch API and download clips.

My program is two parts

1- A web scraper that generates a .json file with all details needed to download files and

2 - A downloader.

Part 1 works perfectly fine and generates the .json files no trouble.

My Downloader contains reference to a Data class that is a handler for common properties and methods like my ClientID, Authentication, OutputPath, JsonFile, QueryURL. It also contains methods to give values to these properties.

Here are the two methods of my FileDownloader.cs that are the problem:

public async static void DownloadAllFiles(Data clientData)
{
    data = clientData;

    data.OutputFolderExists();


    // Deserialize .json file and get ClipInfo list
    List<ClipInfo> clips = JsonConvert.DeserializeObject<List<ClipInfo>>(File.ReadAllText(data.JsonFile));
            
    tasks = new List<Task>();

    foreach(ClipInfo clip in clips)
    {
        tasks.Add(DownloadFilesAsync(clip));
    }

    await Task.WhenAll(tasks);
}

private async static Task DownloadFilesAsync(ClipInfo clip)
{
    WebClient client = new WebClient();
    string url = GetClipURL(clip);
    string filepath = data.OutputPath + clip.id + ".mp4";

    await client.DownloadFileTaskAsync(new Uri(url), filepath);
}

This is only one of my many attempts of downloading files, one which I got the idea from this post:

stackoverflow_link

I have also tried methods like the following from a YouTube video by IAmTimCorey:

video_link

I have spent many an hour tackling this problem, and I honestly can't figure out why it won't work with any of my attempts. I would vastly appreciate your help.

Thanks,

Ben

Below is the entirety of my code, should anyone need it for any reason.

Code Structure:

Project_Structure

The only external libraries I have downloaded is Newtonsoft.Json

ClipInfo.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace Downloader
{
    public class ClipInfo
    {
        public string id { get; set; }
        public string url { get; set; }
        public string embed_url { get; set; }
        public string broadcaster_id { get; set; }
        public string broadcaster_name { get; set; }
        public string creator_id { get; set; }
        public string creator_name { get; set; }
        public string video_id { get; set; }
        public string game_id { get; set; }
        public string language { get; set; }
        public string title { get; set; }
        public int view_count { get; set; }
        public DateTime created_at { get; set; }
        public string thumbnail_url { get; set; }
    }
}

Pagination.cs

namespace Downloader
{
    public class Pagination
    {
        public string cursor { get; set; }
    }

}

Root.cs

using System.Collections.Generic;

namespace Downloader
{
    public class Root
    {
        public List<ClipInfo> data { get; set; }
        public Pagination pagination { get; set; }
    }
}

Data.cs

using System;
using System.IO;

namespace Downloader
{
    public class Data
    {
        private static string directory = Directory.GetCurrentDirectory();
        private readonly static string defaultJsonFile = directory + @"\clips.json";
        private readonly static string defaultOutputPath = directory + @"\Clips\";
        private readonly static string clipsLink = "https://api.twitch.tv/helix/clips?";

        public string OutputPath { get; set; }
        public string JsonFile { get; set; }
        public string ClientID { get; private set; }
        public string Authentication { get; private set; }
        public string QueryURL { get; private set; }
    

        public Data()
        {
            OutputPath = defaultOutputPath;
            JsonFile = defaultJsonFile;
        }
        public Data(string clientID, string authentication)
        {
            ClientID = clientID;
            Authentication = authentication;
            OutputPath = defaultOutputPath;
            JsonFile = defaultJsonFile;
        }
        public Data(string clientID, string authentication, string outputPath)
        {
            ClientID = clientID;
            Authentication = authentication;
            OutputPath = directory + @"\" + outputPath + @"\";
            JsonFile = OutputPath + outputPath + ".json";
        }

        public void GetQuery()
        {
            Console.Write("Please enter your query: ");
            QueryURL = clipsLink + Console.ReadLine();
        }

        public void GetClientID()
        {
            Console.WriteLine("Enter your client ID");
            ClientID = Console.ReadLine();
        }

        public void GetAuthentication()
        {
            Console.WriteLine("Enter your Authentication");
            Authentication = Console.ReadLine();
        }

        public void OutputFolderExists()
        {
            if (!Directory.Exists(OutputPath))
            {
                Directory.CreateDirectory(OutputPath);
            }
        }

    }
}

JsonGenerator.cs

using System;
using System.IO;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Linq;


namespace Downloader
{
    public static class JsonGenerator
    {
        // This class has no constructor.
        // You call the Generate methods, passing in all required data.
        // The file will then be generated.
        private static Data data;

        public static async Task Generate(Data clientData)
        {
            data = clientData;
            string responseContent = null;

            // Loop that runs until the api request goes through
            bool authError = true;
            while (authError)
            {
                authError = false;
                try
                {
                    responseContent = await GetHttpResponse();
                }
                catch (HttpRequestException)
                {
                    Console.WriteLine("Invalid authentication, please enter client-ID and authentication again!");
                    data.GetClientID();
                    data.GetAuthentication();

                    authError = true;
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message);
                    authError = true;
                }
            }

            data.OutputFolderExists();
            GenerateJson(responseContent);
        }

        // Returns the contents of the resopnse to the api call as a string
        private static async Task<string> GetHttpResponse()
        {
            // Creating client
            HttpClient client = new HttpClient();

            if (data.QueryURL == null)
            {
                data.GetQuery();
            }


            // Setting up request
            HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, data.QueryURL);

            // Adding Headers to request
            requestMessage.Headers.Add("client-id", data.ClientID);
            requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", data.Authentication);

            // Receiving response to the request
            HttpResponseMessage responseMessage = await client.SendAsync(requestMessage);

            // Gets the content of the response as a string
            string responseContent = await responseMessage.Content.ReadAsStringAsync();

            return responseContent;
        }

        // Generates or adds to the .json file that contains data on each clip
        private static void GenerateJson(string responseContent)
        {
            // Parses the data from the response to the api request
            Root responseResult = JsonConvert.DeserializeObject<Root>(responseContent);

            // If the file doesn't exist, we need to create it and add a '[' at the start
            if (!File.Exists(data.JsonFile))
            {
                FileStream file = File.Create(data.JsonFile);
                file.Close();
                // The array of json objects needs to be wrapped inside []
                File.AppendAllText(data.JsonFile, "[\n");
            }
            else
            {
                // For a pre-existing .json file, The last object won't have a comma at the
                // end of it so we need to add it now, before we add more objects
                string[] jsonLines = File.ReadAllLines(data.JsonFile);
                File.WriteAllLines(data.JsonFile, jsonLines.Take(jsonLines.Length - 1).ToArray());
                File.AppendAllText(data.JsonFile, ",");
            }

            // If the file already exists, but there was no [ at the start for whatever reason,
            // we need to add it
            if (File.ReadAllText(data.JsonFile).Length == 0 || File.ReadAllText(data.JsonFile)[0] != '[')
            {
                File.WriteAllText(data.JsonFile, "[\n" + File.ReadAllText(data.JsonFile));
            }

            string json;

            // Loops through each ClipInfo object that the api returned
            for (int i = 0; i < responseResult.data.Count; i++)
            {
                // Serializes the ClipInfo object into a json style string
                json = JsonConvert.SerializeObject(responseResult.data[i]);

                // Adds the serialized contents of ClipInfo to the .json file
                File.AppendAllText(data.JsonFile, json);

                if (i != responseResult.data.Count - 1)
                {
                    // All objects except the last require a comma at the end of the
                    // object in order to correctly format the array of json objects
                    File.AppendAllText(data.JsonFile, ",");
                }

                // Adds new line after object entry
                File.AppendAllText(data.JsonFile, "\n");
            }
            // Adds the ] at the end of the file to close off the json objects array
            File.AppendAllText(data.JsonFile, "]");
        }
    }
}

FileDownloader.cs

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;

namespace Downloader
{
    public class FileDownloader
    {
        private static Data data;
        private static List<Task> tasks;
        public async static void DownloadAllFiles(Data clientData)
        {
            data = clientData;

            data.OutputFolderExists();


            // Deserialize .json file and get ClipInfo list
            List<ClipInfo> clips = JsonConvert.DeserializeObject<List<ClipInfo>>(File.ReadAllText(data.JsonFile));

            tasks = new List<Task>();

            foreach (ClipInfo clip in clips)
            {
                tasks.Add(DownloadFilesAsync(clip));
            }

            await Task.WhenAll(tasks);
        }

        private static void GetData()
        {
            if (data.ClientID == null)
            {
                data.GetClientID();
            }
            if (data.Authentication == null)
            {
                data.GetAuthentication();
            }
            if (data.QueryURL == null)
            {
                data.GetQuery();
            }
        }

        private static string GetClipURL(ClipInfo clip)
        {
            // Example thumbnail URL:
            // https://clips-media-assets2.twitch.tv/AT-cm%7C902106752-preview-480x272.jpg
            // You can get the URL of the location of clip.mp4
            // by removing the -preview.... from the thumbnail url */

            string url = clip.thumbnail_url;
            url = url.Substring(0, url.IndexOf("-preview")) + ".mp4";
            return url;
        }
            
        private async static Task DownloadFilesAsync(ClipInfo clip)
        {
            WebClient client = new WebClient();
            string url = GetClipURL(clip);
            string filepath = data.OutputPath + clip.id + ".mp4";

            await client.DownloadFileTaskAsync(new Uri(url), filepath);
        }

        private static void FileDownloadComplete(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
        {
            tasks.Remove((Task)sender);
        }
    }
}

Program.cs

using System;
using System.Threading.Tasks;
using Downloader;

namespace ClipDownloader
{
    class Program
    {
        private static string clientID = "{your_client_id}";
        private static string authentication = "{your_authentication}";
        async static Task Main(string[] args)
        {
            Console.WriteLine("Enter your output path");
            string outputPath = Console.ReadLine();


            Data data = new Data(clientID, authentication, outputPath);
            Console.WriteLine(data.OutputPath);

            //await JsonGenerator.Generate(data);
            FileDownloader.DownloadAllFiles(data);
        }
    }
}

The example query I usually type in is "game_id=510218"

BenWornes
  • 91
  • 1
  • 1
  • 11
  • It doesn't look like you are starting the tasks. Try creating a new task object equal to DownloadFilesAsync(clip) and then task.Start(). You can then add it to your list and wait for the list to complete. Check out this question for more about executing a list of tasks: https://stackoverflow.com/questions/22377533/c-sharp-build-a-list-of-tasks-before-executing – arc-menace Nov 01 '20 at 08:03
  • 2
    what does *doesn't work* mean? Does it throw an error? Does it not seem to download any files? Did you try setting breakpoints if for instance the method `DownloadFilesAsync` is even called? – derpirscher Nov 01 '20 at 08:06
  • @derpirscher In every possible version of parallel downloading I've tried, doesn't work means that the program finishes running, without the downloads having finished. In this particular version, as soon as I type in the query, the command window instantly closes and the program stops. It seems to start each download as they all appear in the folder, but it does not complete them and they download as .mp4s with 0 bytes. – BenWornes Nov 01 '20 at 08:10
  • @arc-menace when I implement your idea, I get the following error: System.InvalidOperationException: 'Start may not be called on a promise-style task.' – BenWornes Nov 01 '20 at 08:13
  • I don't see any form of error handling in your downloading part. Maybe there is an exception? You should add some exception handling – derpirscher Nov 01 '20 at 08:13
  • @BenWornes i would say is less messy to put code if u make a github project – nalnpir Nov 01 '20 at 08:15
  • @derpirscher I removed the error handling for clarity, it didn't seem to have any effect. I realise now that I probably should have left it in – BenWornes Nov 01 '20 at 08:16
  • @nalnpir I do have a github repository. I thought I read somewhere a long time ago that you weren't allowed to put links to github repos in stackoverflow. I'm guessing now, that that is false. The link to the github repo is here: https://github.com/benwornes/TwitchClipDownloader – BenWornes Nov 01 '20 at 08:17
  • 1
    There is too much code in this question. If you could simplified it and reduced it to a minimal reproducible example, it would be easier for anyone to locate the bug, including yourself! – Theodor Zoulias Nov 01 '20 at 10:04
  • @TheodorZoulias All the relevant code was supplied in the main body of the question. I just added the rest of the project at the end in case anyone wanted to test it out for themselves. – BenWornes Nov 01 '20 at 20:24
  • BenWornes ahh, OK, I didn't realize it. Still one shouldn't have to figure out what the `ClipInfo` is and how it interacts with the `Downloader`, in order to get into position to answer the question. Because then the experience becomes less "answering a question" and more like "participating in a debugging session". If I am not wrong the whole deserialization part is not essential for reproducing the problem, and could be omitted. – Theodor Zoulias Nov 02 '20 at 11:55

2 Answers2

3

async void is your problem

Change

public static async void DownloadAllFiles(Data clientData)

To

public static async Task DownloadAllFiles(Data clientData)

Then you can await it

await FileDownloader.DownloadAllFiles(data);

The longer story:

async void runs unobserved (fire and forget). You can't wait for them to finish. In essence as soon as your program starts the task, it finishes, and tears down the App Domain and all your sub tasks, leading you to believe nothing is working.

halfer
  • 19,824
  • 17
  • 99
  • 186
TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • I just tried this, but it didn't work. The program still closes without waiting for the files to download. – BenWornes Nov 01 '20 at 08:29
  • @BenWornes I find that a little unlikely based on your code. – TheGeneral Nov 01 '20 at 08:32
  • @BenWornes i was working in the same that him, i got it working on my computer. When you are using async you should use async all the way. Also i recommend putting the WebClient in an using statement, perhaps thats the reason why it doesnt work in the general version. I ll pull request with a working code but i didnt do much more than the general suggests – nalnpir Nov 01 '20 at 08:38
  • @TheGeneral Wait, I think I fixed it. I tried your changes **after** also trying a change above. It seems the first change counteracted yours and made it not work. After reverting back to my original code, I believe that your answer solved my problem. Thank you so much, such a simple change, yet it left me stumped for quite some time. – BenWornes Nov 01 '20 at 08:40
-3

I'm trying to stay on topic here as best as I can, but when using JsonConvert.DeserializeObject{T}, isn't T suppose to be an encapsulating root object type? I have never used it the way you're using it, so I'm just curious if that might be your bug. I could be completely wrong, and spare me if i am, but JSON is key:value based. Deserializing directly to a List doesn't really make sense. Unless there is a special case in the deserializer? List would be a file that's purely an array of ClipInfo values being deserialized into the members of List{T}(private T[] _items, private int _size, etc.) It needs a parent root object.

// current JSON file format implication(which i dont think is valid JSON?(correct me please) 
clips:
[
  // clip 1
  {  "id": "", "url": "" },

  // clip N
  {  "id": "", "url": "" },
]

// correct(?) JSON file format
{ // { } is the outer encasing object
    clips:
    [
        // clip 1
        {  "id": "", "url": "" },

        // clip N
        {  "id": "", "url": "" },
    ]
}

class ClipInfoJSONFile
{
    public List<ClipInfo> Info { get; set; }
}

var clipInfoList = JsonConverter.DeserializeObject<ClipInfoJSONFile>(...);
  • No.... you can totally deserialize a json array to a collection type fine – Milney Nov 01 '20 at 08:43
  • When originally creating the .json file, I do indeed have to use a Root class which contains List and Pagination. However, I only write the List to the .json file when generating it in the OutputPath directory. This means that I can simply deserialize the entire json into List when it comes to the downloading stage. – BenWornes Nov 01 '20 at 08:44
  • makes total sense now that i see the format allows beginning immediately with an array. good to know. – jeff cassar Nov 01 '20 at 08:56