C # возникают проблемы с асинхронной загрузкой нескольких файлов параллельно в консольном приложении

Прежде чем вы все начнете буйствовать о том, что это дублирующий вопрос, я провел два дня, работая над этой проблемой, просматривая учебные пособия на YouTube по асинхронному программированию, просматривая похожие сообщения о стеке и т. д., и я не могу в жизни понять, как применить асинхронную параллельную загрузку файлов в мой проект.

Сначала немного предыстории:

Я создаю программу, которая при вводе запроса через пользователя будет вызывать API-интерфейс twitch и загружать клипы.

Моя программа состоит из двух частей

1- Веб-скрапер, который создает файл .json со всеми данными, необходимыми для загрузки файлов и

2 - Загрузчик.

Часть 1 работает отлично и без проблем генерирует файлы .json.

My Downloader содержит ссылку на класс Data, который является обработчиком общих свойств и методов, таких как мои ClientID, Authentication, OutputPath, JsonFile, QueryURL. Он также содержит методы для присвоения значений этим свойствам.

Вот два метода моего FileDownloader.cs, которые являются проблемой:

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);
}

Это только одна из моих многочисленных попыток загрузки файлов, идею которой я почерпнул из этого поста:

stackoverflow_link

Я также пробовал следующие методы из видео на YouTube от IAmTimCorey:

ссылка на видео

Я потратил много часов на решение этой проблемы, и я, честно говоря, не могу понять, почему это не сработает ни с одной из моих попыток. Я был бы очень признателен за вашу помощь.

Спасибо,

Бен

Ниже приведен весь мой код, если он кому-то понадобится по какой-либо причине.

Структура кода:

Проект_Структура

Единственные внешние библиотеки, которые я загрузил, — это 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);
        }
    }
}

Я обычно ввожу пример запроса game_id=510218.


person BenWornes    schedule 01.11.2020    source источник
comment
Не похоже, что вы начинаете задачи. Попробуйте создать новый объект задачи, равный DownloadFilesAsync(clip), а затем task.Start(). Затем вы можете добавить его в свой список и дождаться завершения списка. Ознакомьтесь с этим вопросом, чтобы узнать больше о выполнении списка задач: stackoverflow.com/questions/22377533/   -  person arc-menace    schedule 01.11.2020
comment
что означает не работает? Выдает ошибку? Кажется, он не загружает файлы? Вы пытались установить точки останова, если, например, метод DownloadFilesAsync даже вызывается?   -  person derpirscher    schedule 01.11.2020
comment
@derpirscher Во всех возможных версиях параллельной загрузки, которые я пробовал, не работает означает, что программа завершает работу без завершения загрузки. В этой конкретной версии, как только я набираю запрос, окно команд мгновенно закрывается и программа останавливается. Вроде начинает каждую загрузку как они все появляются в папке, но не завершает их и скачивает как .mp4 с 0 байт.   -  person BenWornes    schedule 01.11.2020
comment
@arc-menace, когда я реализую вашу идею, я получаю следующую ошибку: System.InvalidOperationException: «Запуск не может быть вызван для задачи в стиле обещания».   -  person BenWornes    schedule 01.11.2020
comment
Я не вижу какой-либо формы обработки ошибок в вашей части загрузки. Может есть исключение? Вы должны добавить некоторую обработку исключений   -  person derpirscher    schedule 01.11.2020
comment
@BenWornes, я бы сказал, менее грязно вставлять код, если вы делаете проект на github.   -  person nalnpir    schedule 01.11.2020
comment
@derpirscher Я удалил обработку ошибок для ясности, похоже, это не имело никакого эффекта. Теперь я понимаю, что я, вероятно, должен был оставить это в   -  person BenWornes    schedule 01.11.2020
comment
@nalnpir У меня есть репозиторий на github. Я думал, что давным-давно где-то читал, что вам не разрешено размещать ссылки на репозитории github в stackoverflow. Теперь я предполагаю, что это ложь. Ссылка на репозиторий github находится здесь: github.com/benwornes/TwitchClipDownloader.   -  person BenWornes    schedule 01.11.2020
comment
В этом вопросе слишком много кода. Если бы вы могли упростить его и сократить до минимального воспроизводимого примера, всем было бы легче найти ошибку, включая вас!   -  person Theodor Zoulias    schedule 01.11.2020
comment
@TheodorZoulias Весь соответствующий код был предоставлен в основной части вопроса. Я просто добавил остальную часть проекта в конце на случай, если кто-то захочет проверить это на себе.   -  person BenWornes    schedule 01.11.2020
comment
BenWornes ааа, хорошо, я не понял. Тем не менее, не нужно выяснять, что такое ClipInfo и как он взаимодействует с Downloader, чтобы занять позицию для ответа на вопрос. Потому что тогда опыт становится не столько ответом на вопрос, сколько участием в сеансе отладки. Если я не ошибаюсь, вся часть десериализации не важна для воспроизведения проблемы и может быть опущена.   -  person Theodor Zoulias    schedule 02.11.2020


Ответы (2)


async void это твоя проблема

Изменять

public static async void DownloadAllFiles(Data clientData)

To

public static async Task DownloadAllFiles(Data clientData)

Тогда вы можете ждать его

await FileDownloader.DownloadAllFiles(data);

Более длинная история:

async void работает незаметно (выстрелил и забыл). Вы не можете дождаться, когда они закончатся. По сути, как только ваша программа запускает задачу, она завершается и удаляет домен приложения и все ваши подзадачи, заставляя вас поверить, что ничего не работает.

person TheGeneral    schedule 01.11.2020
comment
Я только что попробовал это, но это не сработало. Программа все равно закрывается, не дожидаясь загрузки файлов. - person BenWornes; 01.11.2020
comment
@BenWornes Я считаю это маловероятным, основываясь на вашем коде. - person TheGeneral; 01.11.2020
comment
@BenWornes, я работал в том же, что и он, у меня все работало на моем компьютере. Когда вы используете асинхронность, вы должны использовать асинхронность полностью. Также я рекомендую поместить WebClient в оператор using, возможно, именно поэтому он не работает в общей версии. Я вытащу запрос с рабочим кодом, но я сделал не намного больше, чем предлагает генерал. - person nalnpir; 01.11.2020
comment
@TheGeneral Подождите, я думаю, что исправил это. Я попробовал ваши изменения после и внес изменения выше. Кажется, первое изменение противодействовало вашему и заставило его не работать. Вернувшись к моему исходному коду, я считаю, что ваш ответ решил мою проблему. Большое спасибо, такое простое изменение, но оно поставило меня в тупик на некоторое время. - person BenWornes; 01.11.2020

Я пытаюсь оставаться в теме, насколько это возможно, но при использовании JsonConvert.DeserializeObject{T} не предполагается ли, что T является инкапсулирующим корневым типом объекта? Я никогда не использовал его так, как вы, поэтому мне просто любопытно, может ли это быть вашей ошибкой. Я могу быть совершенно неправ, и пощадите меня, если я ошибаюсь, но JSON основан на ключе: значение. Десериализация непосредственно в список не имеет смысла. Разве в десериализаторе есть особый случай? Список будет файлом, который представляет собой просто массив значений ClipInfo, десериализуемых в члены List{T}(private T[] _items, private int _size и т. д.). Ему нужен родительский корневой объект.

// 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>(...);
person jeff cassar    schedule 01.11.2020
comment
Нет... вы можете полностью десериализовать массив json в тип коллекции. - person Milney; 01.11.2020
comment
При первоначальном создании файла .json мне действительно нужно использовать класс Root, который содержит List‹ClipInfo› и Pagination. Однако я записываю List‹ClipInfo› в файл .json только при его создании в каталоге OutputPath. Это означает, что я могу просто десериализовать весь json в List‹ClipInfo›, когда дело доходит до этапа загрузки. - person BenWornes; 01.11.2020
comment
имеет смысл теперь, когда я вижу, что формат позволяет начинать сразу с массива. хорошо знать. - person jeff cassar; 01.11.2020