public async Task DownloadAsync(IProgress <ProgressReport> progress, CancellationToken cancellationToken)
        {
            using (WebClient client = new WebClient())
            {
                client.Encoding = Encoding.UTF8;
                client.Headers.Add("Accept", "application/vnd.twitchtv.v5+json; charset=UTF-8");
                client.Headers.Add("Client-Id", "kimne78kx3ncx6brgo4mv6wki5h1ko");

                DownloadType downloadType = downloadOptions.Id.All(x => Char.IsDigit(x)) ? DownloadType.Video : DownloadType.Clip;
                string       videoId      = "";

                JObject result   = new JObject();
                JObject video    = new JObject();
                JObject streamer = new JObject();
                JArray  comments = new JArray();

                double videoStart    = 0.0;
                double videoEnd      = 0.0;
                double videoDuration = 0.0;

                if (downloadType == DownloadType.Video)
                {
                    videoId = downloadOptions.Id;
                    JObject taskInfo = await TwitchHelper.GetVideoInfo(Int32.Parse(videoId));

                    streamer["name"] = taskInfo["channel"]["display_name"];
                    streamer["id"]   = taskInfo["channel"]["_id"];
                    videoStart       = downloadOptions.CropBeginning ? downloadOptions.CropBeginningTime : 0.0;
                    videoEnd         = downloadOptions.CropEnding ? downloadOptions.CropEndingTime : taskInfo["length"].ToObject <double>();
                }
                else
                {
                    JObject taskInfo = await TwitchHelper.GetClipInfo(downloadOptions.Id);

                    videoId = taskInfo["vod"]["id"].ToString();
                    downloadOptions.CropBeginning     = true;
                    downloadOptions.CropBeginningTime = taskInfo["vod"]["offset"].ToObject <int>();
                    downloadOptions.CropEnding        = true;
                    downloadOptions.CropEndingTime    = downloadOptions.CropBeginningTime + taskInfo["duration"].ToObject <double>();
                    streamer["name"] = taskInfo["broadcaster"]["display_name"];
                    streamer["id"]   = taskInfo["broadcaster"]["id"];
                    videoStart       = taskInfo["vod"]["offset"].ToObject <double>();
                    videoEnd         = taskInfo["vod"]["offset"].ToObject <double>() + taskInfo["duration"].ToObject <double>();
                }

                video["start"] = videoStart;
                video["end"]   = videoEnd;
                videoDuration  = videoEnd - videoStart;

                double latestMessage = videoStart - 1;
                bool   isFirst       = true;
                string cursor        = "";

                while (latestMessage < videoEnd)
                {
                    string response;
                    if (isFirst)
                    {
                        response = await client.DownloadStringTaskAsync(String.Format("https://api.twitch.tv/v5/videos/{0}/comments?content_offset_seconds={1}", videoId, videoStart));
                    }
                    else
                    {
                        response = await client.DownloadStringTaskAsync(String.Format("https://api.twitch.tv/v5/videos/{0}/comments?cursor={1}", videoId, cursor));
                    }

                    JObject res = JObject.Parse(response);

                    foreach (var comment in res["comments"])
                    {
                        if (latestMessage < videoEnd && comment["content_offset_seconds"].ToObject <double>() > videoStart)
                        {
                            comments.Add(comment);
                        }

                        latestMessage = comment["content_offset_seconds"].ToObject <double>();
                    }
                    if (res["_next"] == null)
                    {
                        break;
                    }
                    else
                    {
                        cursor = res["_next"].ToString();
                    }

                    int percent = (int)Math.Floor((latestMessage - videoStart) / videoDuration * 100);
                    progress.Report(new ProgressReport()
                    {
                        reportType = ReportType.Percent, data = percent
                    });
                    progress.Report(new ProgressReport()
                    {
                        reportType = ReportType.Message, data = $"Downloading {percent}%"
                    });

                    cancellationToken.ThrowIfCancellationRequested();

                    if (isFirst)
                    {
                        isFirst = false;
                    }
                }

                result["streamer"] = streamer;
                result["comments"] = comments;
                result["video"]    = video;

                if (downloadOptions.EmbedEmotes && downloadOptions.IsJson)
                {
                    progress.Report(new ProgressReport()
                    {
                        reportType = ReportType.Message, data = "Downloading + Embedding Emotes"
                    });
                    result["emotes"] = new JObject();
                    JArray firstParty = new JArray();
                    JArray thirdParty = new JArray();

                    string cacheFolder = Path.Combine(Path.GetTempPath(), "TwitchDownloader", "cache");
                    List <ThirdPartyEmote> thirdPartyEmotes = new List <ThirdPartyEmote>();
                    List <KeyValuePair <string, SKBitmap> > firstPartyEmotes = new List <KeyValuePair <string, SKBitmap> >();

                    await Task.Run(() => {
                        thirdPartyEmotes = TwitchHelper.GetThirdPartyEmotes(streamer["id"].ToObject <int>(), cacheFolder);
                        firstPartyEmotes = TwitchHelper.GetEmotes(result["comments"].ToObject <List <Comment> >(), cacheFolder).ToList();
                    });

                    foreach (ThirdPartyEmote emote in thirdPartyEmotes)
                    {
                        JObject newEmote = new JObject();
                        newEmote["id"]         = emote.id;
                        newEmote["imageScale"] = emote.imageScale;
                        newEmote["data"]       = emote.imageData;
                        newEmote["name"]       = emote.name;
                        thirdParty.Add(newEmote);
                    }
                    foreach (KeyValuePair <string, SKBitmap> emote in firstPartyEmotes)
                    {
                        JObject newEmote = new JObject();
                        newEmote["id"]         = emote.Key;
                        newEmote["imageScale"] = 1;
                        newEmote["data"]       = SKImage.FromBitmap(emote.Value).Encode(SKEncodedImageFormat.Png, 100).ToArray();
                        firstParty.Add(newEmote);
                    }

                    result["emotes"]["thirdParty"] = thirdParty;
                    result["emotes"]["firstParty"] = firstParty;
                }

                using (StreamWriter sw = new StreamWriter(downloadOptions.Filename))
                {
                    if (downloadOptions.IsJson)
                    {
                        sw.Write(result.ToString(Formatting.None));
                    }
                    else
                    {
                        foreach (var comment in result["comments"])
                        {
                            string username = comment["commenter"]["display_name"].ToString();
                            string message  = comment["message"]["body"].ToString();
                            if (downloadOptions.Timestamp)
                            {
                                string timestamp = comment["created_at"].ToObject <DateTime>().ToString("u").Replace("Z", " UTC");
                                sw.WriteLine(String.Format("[{0}] {1}: {2}", timestamp, username, message));
                            }
                            else
                            {
                                sw.WriteLine(String.Format("{0}: {1}", username, message));
                            }
                        }
                    }

                    sw.Flush();
                    sw.Close();
                    result = null;
                }
            }
        }
示例#2
0
        public async Task DownloadAsync(IProgress <ProgressReport> progress, CancellationToken cancellationToken)
        {
            using (WebClient client = new WebClient())
            {
                client.Encoding = Encoding.UTF8;
                client.Headers.Add("Accept", "application/vnd.twitchtv.v5+json; charset=UTF-8");
                client.Headers.Add("Client-Id", "kimne78kx3ncx6brgo4mv6wki5h1ko");

                DownloadType downloadType = downloadOptions.Id.All(x => Char.IsDigit(x)) ? DownloadType.Video : DownloadType.Clip;
                string       videoId      = "";

                List <Comment> comments = new List <Comment>();
                ChatRoot       chatRoot = new ChatRoot()
                {
                    streamer = new Streamer(), video = new VideoTime(), comments = comments
                };

                double videoStart    = 0.0;
                double videoEnd      = 0.0;
                double videoDuration = 0.0;
                int    errorCount    = 0;

                if (downloadType == DownloadType.Video)
                {
                    videoId = downloadOptions.Id;
                    GqlVideoResponse taskInfo = await TwitchHelper.GetVideoInfo(Int32.Parse(videoId));

                    chatRoot.streamer.name = taskInfo.data.video.owner.displayName;
                    chatRoot.streamer.id   = int.Parse(taskInfo.data.video.owner.id);
                    videoStart             = downloadOptions.CropBeginning ? downloadOptions.CropBeginningTime : 0.0;
                    videoEnd = downloadOptions.CropEnding ? downloadOptions.CropEndingTime : taskInfo.data.video.lengthSeconds;
                }
                else
                {
                    GqlClipResponse taskInfo = await TwitchHelper.GetClipInfo(downloadOptions.Id);

                    if (taskInfo.data.clip.video == null || taskInfo.data.clip.videoOffsetSeconds == null)
                    {
                        throw new Exception("Invalid VOD for clip, deleted/expired VOD possibly?");
                    }

                    videoId = taskInfo.data.clip.video.id;
                    downloadOptions.CropBeginning     = true;
                    downloadOptions.CropBeginningTime = (int)taskInfo.data.clip.videoOffsetSeconds;
                    downloadOptions.CropEnding        = true;
                    downloadOptions.CropEndingTime    = downloadOptions.CropBeginningTime + taskInfo.data.clip.durationSeconds;
                    chatRoot.streamer.name            = taskInfo.data.clip.broadcaster.displayName;
                    chatRoot.streamer.id = int.Parse(taskInfo.data.clip.broadcaster.id);
                    videoStart           = (int)taskInfo.data.clip.videoOffsetSeconds;
                    videoEnd             = (int)taskInfo.data.clip.videoOffsetSeconds + taskInfo.data.clip.durationSeconds;
                }

                chatRoot.video.start = videoStart;
                chatRoot.video.end   = videoEnd;
                videoDuration        = videoEnd - videoStart;

                double latestMessage = videoStart - 1;
                bool   isFirst       = true;
                string cursor        = "";

                while (latestMessage < videoEnd)
                {
                    string response;

                    try
                    {
                        if (isFirst)
                        {
                            response = await client.DownloadStringTaskAsync(String.Format("https://api.twitch.tv/v5/videos/{0}/comments?content_offset_seconds={1}", videoId, videoStart));
                        }
                        else
                        {
                            response = await client.DownloadStringTaskAsync(String.Format("https://api.twitch.tv/v5/videos/{0}/comments?cursor={1}", videoId, cursor));
                        }
                        errorCount = 0;
                    }
                    catch (WebException ex)
                    {
                        await Task.Delay(1000 *errorCount);

                        errorCount++;

                        if (errorCount >= 10)
                        {
                            throw ex;
                        }

                        continue;
                    }

                    CommentResponse commentResponse = JsonConvert.DeserializeObject <CommentResponse>(response);

                    foreach (var comment in commentResponse.comments)
                    {
                        if (latestMessage < videoEnd && comment.content_offset_seconds > videoStart)
                        {
                            comments.Add(comment);
                        }

                        latestMessage = comment.content_offset_seconds;
                    }
                    if (commentResponse._next == null)
                    {
                        break;
                    }
                    else
                    {
                        cursor = commentResponse._next;
                    }

                    int percent = (int)Math.Floor((latestMessage - videoStart) / videoDuration * 100);
                    progress.Report(new ProgressReport()
                    {
                        reportType = ReportType.Percent, data = percent
                    });
                    progress.Report(new ProgressReport()
                    {
                        reportType = ReportType.MessageInfo, data = $"Downloading {percent}%"
                    });

                    cancellationToken.ThrowIfCancellationRequested();

                    if (isFirst)
                    {
                        isFirst = false;
                    }
                }

                if (downloadOptions.EmbedEmotes && downloadOptions.IsJson)
                {
                    progress.Report(new ProgressReport()
                    {
                        reportType = ReportType.Message, data = "Downloading + Embedding Emotes"
                    });
                    chatRoot.emotes = new Emotes();
                    List <FirstPartyEmoteData> firstParty = new List <FirstPartyEmoteData>();
                    List <ThirdPartyEmoteData> thirdParty = new List <ThirdPartyEmoteData>();

                    string             cacheFolder      = Path.Combine(Path.GetTempPath(), "TwitchDownloader", "cache");
                    List <TwitchEmote> thirdPartyEmotes = new List <TwitchEmote>();
                    List <TwitchEmote> firstPartyEmotes = new List <TwitchEmote>();

                    await Task.Run(() => {
                        thirdPartyEmotes = TwitchHelper.GetThirdPartyEmotes(chatRoot.streamer.id, cacheFolder);
                        firstPartyEmotes = TwitchHelper.GetEmotes(comments, cacheFolder).ToList();
                    });

                    foreach (TwitchEmote emote in thirdPartyEmotes)
                    {
                        ThirdPartyEmoteData newEmote = new ThirdPartyEmoteData();
                        newEmote.id         = emote.id;
                        newEmote.imageScale = emote.imageScale;
                        newEmote.data       = emote.imageData;
                        newEmote.name       = emote.name;
                        thirdParty.Add(newEmote);
                    }
                    foreach (TwitchEmote emote in firstPartyEmotes)
                    {
                        FirstPartyEmoteData newEmote = new FirstPartyEmoteData();
                        newEmote.id         = emote.id;
                        newEmote.imageScale = 1;
                        newEmote.data       = emote.imageData;
                        firstParty.Add(newEmote);
                    }

                    chatRoot.emotes.thirdParty = thirdParty;
                    chatRoot.emotes.firstParty = firstParty;
                }

                if (downloadOptions.IsJson)
                {
                    using (TextWriter writer = File.CreateText(downloadOptions.Filename))
                    {
                        var serializer = new JsonSerializer();
                        serializer.Serialize(writer, chatRoot);
                    }
                }
                else
                {
                    using (StreamWriter sw = new StreamWriter(downloadOptions.Filename))
                    {
                        foreach (var comment in chatRoot.comments)
                        {
                            string username = comment.commenter.display_name;
                            string message  = comment.message.body;
                            if (downloadOptions.TimeFormat == TimestampFormat.Utc)
                            {
                                string timestamp = comment.created_at.ToString("u").Replace("Z", " UTC");
                                sw.WriteLine(String.Format("[{0}] {1}: {2}", timestamp, username, message));
                            }
                            else if (downloadOptions.TimeFormat == TimestampFormat.Relative)
                            {
                                TimeSpan time      = new TimeSpan(0, 0, (int)comment.content_offset_seconds);
                                string   timestamp = time.ToString(@"h\:mm\:ss");
                                sw.WriteLine(String.Format("[{0}] {1}: {2}", timestamp, username, message));
                            }
                            else if (downloadOptions.TimeFormat == TimestampFormat.None)
                            {
                                sw.WriteLine(String.Format("{0}: {1}", username, message));
                            }
                        }

                        sw.Flush();
                        sw.Close();
                    }
                }

                chatRoot = null;
                GC.Collect();
            }
        }
        public async Task DownloadAsync(IProgress <ProgressReport> progress, CancellationToken cancellationToken)
        {
            string tempFolder     = Path.Combine(Path.GetTempPath(), "TwitchDownloader");
            string downloadFolder = Path.Combine(tempFolder, downloadOptions.Id.ToString() == "0" ? Guid.NewGuid().ToString() : downloadOptions.Id.ToString());

            try
            {
                ServicePointManager.DefaultConnectionLimit = downloadOptions.DownloadThreads;

                if (Directory.Exists(downloadFolder))
                {
                    Directory.Delete(downloadFolder, true);
                }
                Directory.CreateDirectory(downloadFolder);

                string playlistUrl;

                if (downloadOptions.PlaylistUrl == null)
                {
                    Task <JObject> taskInfo        = TwitchHelper.GetVideoInfo(downloadOptions.Id);
                    Task <JObject> taskAccessToken = TwitchHelper.GetVideoToken(downloadOptions.Id, downloadOptions.Oauth);
                    await Task.WhenAll(taskInfo, taskAccessToken);

                    string[] videoPlaylist = await TwitchHelper.GetVideoPlaylist(downloadOptions.Id, taskAccessToken.Result["token"].ToString(), taskAccessToken.Result["sig"].ToString());

                    List <KeyValuePair <string, string> > videoQualities = new List <KeyValuePair <string, string> >();

                    for (int i = 0; i < videoPlaylist.Length; i++)
                    {
                        if (videoPlaylist[i].Contains("#EXT-X-MEDIA"))
                        {
                            string lastPart      = videoPlaylist[i].Substring(videoPlaylist[i].IndexOf("NAME=\"") + 6);
                            string stringQuality = lastPart.Substring(0, lastPart.IndexOf("\""));

                            if (!videoQualities.Any(x => x.Key.Equals(stringQuality)))
                            {
                                videoQualities.Add(new KeyValuePair <string, string>(stringQuality, videoPlaylist[i + 2]));
                            }
                        }
                    }

                    if (videoQualities.Any(x => x.Key.Equals(downloadOptions.Quality)))
                    {
                        playlistUrl = videoQualities.Where(x => x.Key.Equals(downloadOptions.Quality)).First().Value;
                    }
                    else
                    {
                        //Unable to find specified quality, defaulting to highest quality
                        playlistUrl = videoQualities.First().Value;
                    }
                }
                else
                {
                    playlistUrl = downloadOptions.PlaylistUrl;
                }

                string baseUrl = playlistUrl.Substring(0, playlistUrl.LastIndexOf("/") + 1);
                List <KeyValuePair <string, double> > videoList = new List <KeyValuePair <string, double> >();

                using (WebClient client = new WebClient())
                {
                    string[] videoChunks = (await client.DownloadStringTaskAsync(playlistUrl)).Split('\n');

                    for (int i = 0; i < videoChunks.Length; i++)
                    {
                        if (videoChunks[i].Contains("#EXTINF"))
                        {
                            if (videoChunks[i + 1].Contains("#EXT-X-BYTERANGE"))
                            {
                                if (videoList.Any(x => x.Key == videoChunks[i + 2]))
                                {
                                    KeyValuePair <string, double> pair = videoList.Where(x => x.Key == videoChunks[i + 2]).First();
                                    pair = new KeyValuePair <string, double>(pair.Key, pair.Value + Double.Parse(videoChunks[i].Remove(0, 8).TrimEnd(','), CultureInfo.InvariantCulture));
                                }
                                else
                                {
                                    videoList.Add(new KeyValuePair <string, double>(videoChunks[i + 2], Double.Parse(videoChunks[i].Remove(0, 8).TrimEnd(','), CultureInfo.InvariantCulture)));
                                }
                            }
                            else
                            {
                                videoList.Add(new KeyValuePair <string, double>(videoChunks[i + 1], Double.Parse(videoChunks[i].Remove(0, 8).TrimEnd(','), CultureInfo.InvariantCulture)));
                            }
                        }
                    }
                }

                List <KeyValuePair <string, double> > videoListCropped = GenerateCroppedVideoList(videoList, downloadOptions);
                Queue <string> videoParts = new Queue <string>();
                videoListCropped.ForEach(x => videoParts.Enqueue(x.Key));
                List <string> videoPartsList = new List <string>(videoParts);
                int           partCount      = videoParts.Count;
                int           doneCount      = 0;

                using (var throttler = new SemaphoreSlim(downloadOptions.DownloadThreads))
                {
                    Task[] downloadTasks = videoParts.Select(request => Task.Run(async() =>
                    {
                        await throttler.WaitAsync();
                        try
                        {
                            bool isDone    = false;
                            int errorCount = 0;
                            while (!isDone && errorCount < 10)
                            {
                                try
                                {
                                    using (WebClient client = new WebClient())
                                    {
                                        await client.DownloadFileTaskAsync(baseUrl + request, Path.Combine(downloadFolder, RemoveQueryString(request)));
                                        isDone = true;
                                    }
                                }
                                catch (WebException ex)
                                {
                                    errorCount++;
                                    Debug.WriteLine(ex);
                                    await Task.Delay(10000);
                                }
                            }

                            if (!isDone)
                            {
                                throw new Exception("Video part " + request + " failed after 10 retries");
                            }

                            doneCount++;
                            int percent = (int)Math.Floor(((double)doneCount / (double)partCount) * 100);
                            progress.Report(new ProgressReport()
                            {
                                reportType = ReportType.Message, data = String.Format("Downloading {0}% (1/3)", percent)
                            });
                            progress.Report(new ProgressReport()
                            {
                                reportType = ReportType.Percent, data = percent
                            });

                            return;
                        }
                        catch (Exception ex)
                        {
                            Debug.WriteLine(ex);
                        }
                        finally
                        {
                            throttler.Release();
                        }
                    })).ToArray();
                    await Task.WhenAll(downloadTasks);
                }

                CheckCancelation(cancellationToken, downloadFolder);

                progress.Report(new ProgressReport()
                {
                    reportType = ReportType.Message, data = "Combining Parts (2/3)"
                });
                progress.Report(new ProgressReport()
                {
                    reportType = ReportType.Percent, data = 0
                });

                await Task.Run(() =>
                {
                    string outputFile = Path.Combine(downloadFolder, "output.ts");
                    using (FileStream outputStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
                    {
                        foreach (var part in videoPartsList)
                        {
                            string file = Path.Combine(downloadFolder, RemoveQueryString(part));
                            if (File.Exists(file))
                            {
                                byte[] writeBytes = File.ReadAllBytes(file);
                                outputStream.Write(writeBytes, 0, writeBytes.Length);

                                try
                                {
                                    File.Delete(file);
                                }
                                catch { }
                            }
                            CheckCancelation(cancellationToken, downloadFolder);
                        }
                    }
                });


                progress.Report(new ProgressReport()
                {
                    reportType = ReportType.Message, data = "Finalizing MP4 (3/3)"
                });

                double startOffset = 0.0;

                for (int i = 0; i < videoList.Count; i++)
                {
                    if (videoList[i].Key == videoPartsList[0])
                    {
                        break;
                    }

                    startOffset += videoList[i].Value;
                }

                double seekTime     = downloadOptions.CropBeginningTime;
                double seekDuration = Math.Round(downloadOptions.CropEndingTime - seekTime);

                await Task.Run(() =>
                {
                    try
                    {
                        var process = new Process
                        {
                            StartInfo =
                            {
                                FileName               = downloadOptions.FfmpegPath,
                                Arguments              = String.Format("-y -avoid_negative_ts make_zero " + (downloadOptions.CropBeginning ? "-ss {1} " : "") + "-i \"{0}\" -analyzeduration {2} -probesize {2} " + (downloadOptions.CropEnding ? "-t {3} " : "") + "-c:v copy \"{4}\"", Path.Combine(downloadFolder, "output.ts"), (seekTime - startOffset).ToString(), Int32.MaxValue, seekDuration.ToString(), Path.GetFullPath(downloadOptions.Filename)),
                                UseShellExecute        = false,
                                CreateNoWindow         = true,
                                RedirectStandardInput  = false,
                                RedirectStandardOutput = false,
                                RedirectStandardError  = false
                            }
                        };
                        process.Start();
                        process.WaitForExit();
                    }
                    catch (TaskCanceledException) { }
                    Cleanup(downloadFolder);
                });
            }
            catch
            {
                Cleanup(downloadFolder);
                throw;
            }
        }
示例#4
0
        public async Task DownloadAsync(IProgress <ProgressReport> progress, CancellationToken cancellationToken)
        {
            using (WebClient client = new WebClient())
            {
                client.Encoding = Encoding.UTF8;
                client.Headers.Add("Accept", "application/vnd.twitchtv.v5+json; charset=UTF-8");
                client.Headers.Add("Client-Id", "kimne78kx3ncx6brgo4mv6wki5h1ko");

                DownloadType downloadType = downloadOptions.Id.All(x => Char.IsDigit(x)) ? DownloadType.Video : DownloadType.Clip;
                string       videoId      = "";

                List <Comment> comments = new List <Comment>();
                ChatRoot       chatRoot = new ChatRoot()
                {
                    streamer = new Streamer(), video = new VideoTime(), comments = comments
                };

                double videoStart    = 0.0;
                double videoEnd      = 0.0;
                double videoDuration = 0.0;

                if (downloadType == DownloadType.Video)
                {
                    videoId = downloadOptions.Id;
                    JObject taskInfo = await TwitchHelper.GetVideoInfo(Int32.Parse(videoId));

                    chatRoot.streamer.name = taskInfo["channel"]["display_name"].ToString();
                    chatRoot.streamer.id   = taskInfo["channel"]["_id"].ToObject <int>();
                    videoStart             = downloadOptions.CropBeginning ? downloadOptions.CropBeginningTime : 0.0;
                    videoEnd = downloadOptions.CropEnding ? downloadOptions.CropEndingTime : taskInfo["length"].ToObject <double>();
                }
                else
                {
                    JObject taskInfo = await TwitchHelper.GetClipInfo(downloadOptions.Id);

                    videoId = taskInfo["vod"]["id"].ToString();
                    downloadOptions.CropBeginning     = true;
                    downloadOptions.CropBeginningTime = taskInfo["vod"]["offset"].ToObject <int>();
                    downloadOptions.CropEnding        = true;
                    downloadOptions.CropEndingTime    = downloadOptions.CropBeginningTime + taskInfo["duration"].ToObject <double>();
                    chatRoot.streamer.name            = taskInfo["broadcaster"]["display_name"].ToString();
                    chatRoot.streamer.id = taskInfo["broadcaster"]["id"].ToObject <int>();
                    videoStart           = taskInfo["vod"]["offset"].ToObject <double>();
                    videoEnd             = taskInfo["vod"]["offset"].ToObject <double>() + taskInfo["duration"].ToObject <double>();
                }

                chatRoot.video.start = videoStart;
                chatRoot.video.end   = videoEnd;
                videoDuration        = videoEnd - videoStart;

                double latestMessage = videoStart - 1;
                bool   isFirst       = true;
                string cursor        = "";

                while (latestMessage < videoEnd)
                {
                    string response;
                    if (isFirst)
                    {
                        response = await client.DownloadStringTaskAsync(String.Format("https://api.twitch.tv/v5/videos/{0}/comments?content_offset_seconds={1}", videoId, videoStart));
                    }
                    else
                    {
                        response = await client.DownloadStringTaskAsync(String.Format("https://api.twitch.tv/v5/videos/{0}/comments?cursor={1}", videoId, cursor));
                    }

                    CommentResponse commentResponse = JsonConvert.DeserializeObject <CommentResponse>(response);

                    foreach (var comment in commentResponse.comments)
                    {
                        if (latestMessage < videoEnd && comment.content_offset_seconds > videoStart)
                        {
                            comments.Add(comment);
                        }

                        latestMessage = comment.content_offset_seconds;
                    }
                    if (commentResponse._next == null)
                    {
                        break;
                    }
                    else
                    {
                        cursor = commentResponse._next;
                    }

                    int percent = (int)Math.Floor((latestMessage - videoStart) / videoDuration * 100);
                    progress.Report(new ProgressReport()
                    {
                        reportType = ReportType.Percent, data = percent
                    });
                    progress.Report(new ProgressReport()
                    {
                        reportType = ReportType.MessageInfo, data = $"Downloading {percent}%"
                    });

                    cancellationToken.ThrowIfCancellationRequested();

                    if (isFirst)
                    {
                        isFirst = false;
                    }
                }

                if (downloadOptions.EmbedEmotes && downloadOptions.IsJson)
                {
                    progress.Report(new ProgressReport()
                    {
                        reportType = ReportType.Message, data = "Downloading + Embedding Emotes"
                    });
                    chatRoot.emotes = new Emotes();
                    List <FirstPartyEmoteData> firstParty = new List <FirstPartyEmoteData>();
                    List <ThirdPartyEmoteData> thirdParty = new List <ThirdPartyEmoteData>();

                    string cacheFolder = Path.Combine(Path.GetTempPath(), "TwitchDownloader", "cache");
                    List <ThirdPartyEmote> thirdPartyEmotes = new List <ThirdPartyEmote>();
                    List <KeyValuePair <string, SKBitmap> > firstPartyEmotes = new List <KeyValuePair <string, SKBitmap> >();

                    await Task.Run(() => {
                        thirdPartyEmotes = TwitchHelper.GetThirdPartyEmotes(chatRoot.streamer.id, cacheFolder);
                        firstPartyEmotes = TwitchHelper.GetEmotes(comments, cacheFolder).ToList();
                    });

                    foreach (ThirdPartyEmote emote in thirdPartyEmotes)
                    {
                        ThirdPartyEmoteData newEmote = new ThirdPartyEmoteData();
                        newEmote.id         = emote.id;
                        newEmote.imageScale = emote.imageScale;
                        newEmote.data       = emote.imageData;
                        newEmote.name       = emote.name;
                        thirdParty.Add(newEmote);
                    }
                    foreach (KeyValuePair <string, SKBitmap> emote in firstPartyEmotes)
                    {
                        FirstPartyEmoteData newEmote = new FirstPartyEmoteData();
                        newEmote.id         = emote.Key;
                        newEmote.imageScale = 1;
                        newEmote.data       = SKImage.FromBitmap(emote.Value).Encode(SKEncodedImageFormat.Png, 100).ToArray();
                        firstParty.Add(newEmote);
                    }

                    chatRoot.emotes.thirdParty = thirdParty;
                    chatRoot.emotes.firstParty = firstParty;
                }

                if (downloadOptions.IsJson)
                {
                    using (TextWriter writer = File.CreateText(downloadOptions.Filename))
                    {
                        var serializer = new JsonSerializer();
                        serializer.Serialize(writer, chatRoot);
                    }
                }
                else
                {
                    using (StreamWriter sw = new StreamWriter(downloadOptions.Filename))
                    {
                        foreach (var comment in chatRoot.comments)
                        {
                            string username = comment.commenter.display_name;
                            string message  = comment.message.body;
                            if (downloadOptions.Timestamp)
                            {
                                string timestamp = comment.created_at.ToString("u").Replace("Z", " UTC");
                                sw.WriteLine(String.Format("[{0}] {1}: {2}", timestamp, username, message));
                            }
                            else
                            {
                                sw.WriteLine(String.Format("{0}: {1}", username, message));
                            }
                        }

                        sw.Flush();
                        sw.Close();
                    }
                }

                chatRoot = null;
                GC.Collect();
            }
        }