diff --git a/TwitchDownloaderCLI/Tools/CacheHandler.cs b/TwitchDownloaderCLI/Modes/CacheHandler.cs similarity index 98% rename from TwitchDownloaderCLI/Tools/CacheHandler.cs rename to TwitchDownloaderCLI/Modes/CacheHandler.cs index 7400eea5..127d4b54 100644 --- a/TwitchDownloaderCLI/Tools/CacheHandler.cs +++ b/TwitchDownloaderCLI/Modes/CacheHandler.cs @@ -2,7 +2,7 @@ using System.IO; using TwitchDownloaderCLI.Modes.Arguments; -namespace TwitchDownloaderCLI.Tools +namespace TwitchDownloaderCLI.Modes { public static class CacheHandler { diff --git a/TwitchDownloaderCLI/Modes/DownloadChat.cs b/TwitchDownloaderCLI/Modes/DownloadChat.cs index bd6a2159..0f995ccb 100644 --- a/TwitchDownloaderCLI/Modes/DownloadChat.cs +++ b/TwitchDownloaderCLI/Modes/DownloadChat.cs @@ -15,10 +15,10 @@ internal static void Download(ChatDownloadArgs inputOptions) { var downloadOptions = GetDownloadOptions(inputOptions); - ChatDownloader chatDownloader = new(downloadOptions); - Progress progress = new(); - progress.ProgressChanged += ProgressHandler.Progress_ProgressChanged; - chatDownloader.DownloadAsync(progress, new CancellationToken()).Wait(); + var progress = new CliTaskProgress(); + + var chatDownloader = new ChatDownloader(downloadOptions, progress); + chatDownloader.DownloadAsync(CancellationToken.None).Wait(); } private static ChatDownloadOptions GetDownloadOptions(ChatDownloadArgs inputOptions) diff --git a/TwitchDownloaderCLI/Modes/DownloadClip.cs b/TwitchDownloaderCLI/Modes/DownloadClip.cs index 321b1b52..7e2dac5f 100644 --- a/TwitchDownloaderCLI/Modes/DownloadClip.cs +++ b/TwitchDownloaderCLI/Modes/DownloadClip.cs @@ -13,17 +13,16 @@ internal static class DownloadClip { internal static void Download(ClipDownloadArgs inputOptions) { + var progress = new CliTaskProgress(); + if (inputOptions.EncodeMetadata == true) { - FfmpegHandler.DetectFfmpeg(inputOptions.FfmpegPath); + FfmpegHandler.DetectFfmpeg(inputOptions.FfmpegPath, progress); } - Progress progress = new(); - progress.ProgressChanged += ProgressHandler.Progress_ProgressChanged; - var downloadOptions = GetDownloadOptions(inputOptions); - ClipDownloader clipDownloader = new(downloadOptions, progress); + var clipDownloader = new ClipDownloader(downloadOptions, progress); clipDownloader.DownloadAsync(new CancellationToken()).Wait(); } diff --git a/TwitchDownloaderCLI/Modes/DownloadVideo.cs b/TwitchDownloaderCLI/Modes/DownloadVideo.cs index c9bedbf9..855ac6d2 100644 --- a/TwitchDownloaderCLI/Modes/DownloadVideo.cs +++ b/TwitchDownloaderCLI/Modes/DownloadVideo.cs @@ -13,22 +13,21 @@ internal static class DownloadVideo { internal static void Download(VideoDownloadArgs inputOptions) { - FfmpegHandler.DetectFfmpeg(inputOptions.FfmpegPath); + var progress = new CliTaskProgress(); - Progress progress = new(); - progress.ProgressChanged += ProgressHandler.Progress_ProgressChanged; + FfmpegHandler.DetectFfmpeg(inputOptions.FfmpegPath, progress); var downloadOptions = GetDownloadOptions(inputOptions); downloadOptions.CacheCleanerCallback = directoryInfos => { - Console.WriteLine( - $"[LOG] - {directoryInfos.Length} unmanaged video caches were found at '{downloadOptions.TempFolder}' and can be safely deleted. " + + progress.LogInfo( + $"{directoryInfos.Length} unmanaged video caches were found at '{downloadOptions.TempFolder}' and can be safely deleted. " + "Run 'TwitchDownloaderCLI cache help' for more information."); return Array.Empty(); }; - VideoDownloader videoDownloader = new(downloadOptions, progress); + var videoDownloader = new VideoDownloader(downloadOptions, progress); videoDownloader.DownloadAsync(new CancellationToken()).Wait(); } diff --git a/TwitchDownloaderCLI/Tools/FfmpegHandler.cs b/TwitchDownloaderCLI/Modes/FfmpegHandler.cs similarity index 90% rename from TwitchDownloaderCLI/Tools/FfmpegHandler.cs rename to TwitchDownloaderCLI/Modes/FfmpegHandler.cs index d971a2be..d012f006 100644 --- a/TwitchDownloaderCLI/Tools/FfmpegHandler.cs +++ b/TwitchDownloaderCLI/Modes/FfmpegHandler.cs @@ -1,15 +1,17 @@ -using Mono.Unix; -using System; +using System; using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; +using Mono.Unix; using TwitchDownloaderCLI.Modes.Arguments; +using TwitchDownloaderCLI.Tools; +using TwitchDownloaderCore.Interfaces; using Xabe.FFmpeg; using Xabe.FFmpeg.Downloader; -namespace TwitchDownloaderCLI.Tools +namespace TwitchDownloaderCLI.Modes { public static class FfmpegHandler { @@ -55,14 +57,14 @@ private static void DownloadFfmpeg() } } - public static void DetectFfmpeg(string ffmpegPath) + public static void DetectFfmpeg(string ffmpegPath, ITaskLogger logger) { if (File.Exists(ffmpegPath) || File.Exists(FfmpegExecutableName) || PathUtils.ExistsOnPATH(FfmpegExecutableName)) { return; } - Console.WriteLine("[ERROR] - Unable to find FFmpeg, exiting. You can download FFmpeg automatically with the command \"TwitchDownloaderCLI ffmpeg -d\""); + logger.LogError("Unable to find FFmpeg, exiting. You can download FFmpeg automatically with the command \"TwitchDownloaderCLI ffmpeg -d\""); Environment.Exit(1); } diff --git a/TwitchDownloaderCLI/Modes/MergeTs.cs b/TwitchDownloaderCLI/Modes/MergeTs.cs index 13c41c65..baacb6f5 100644 --- a/TwitchDownloaderCLI/Modes/MergeTs.cs +++ b/TwitchDownloaderCLI/Modes/MergeTs.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using TwitchDownloaderCLI.Modes.Arguments; using TwitchDownloaderCLI.Tools; using TwitchDownloaderCore; @@ -11,13 +10,13 @@ internal static class MergeTs { internal static void Merge(TsMergeArgs inputOptions) { - Console.WriteLine("[INFO] The TS merger is experimental and is subject to change without notice in future releases."); + var progress = new CliTaskProgress(); - Progress progress = new(); - progress.ProgressChanged += ProgressHandler.Progress_ProgressChanged; + progress.LogInfo("The TS merger is experimental and is subject to change without notice in future releases."); var mergeOptions = GetMergeOptions(inputOptions); - TsMerger tsMerger = new(mergeOptions, progress); + + var tsMerger = new TsMerger(mergeOptions, progress); tsMerger.MergeAsync(new CancellationToken()).Wait(); } diff --git a/TwitchDownloaderCLI/Modes/RenderChat.cs b/TwitchDownloaderCLI/Modes/RenderChat.cs index 085b0a96..c5a65a79 100644 --- a/TwitchDownloaderCLI/Modes/RenderChat.cs +++ b/TwitchDownloaderCLI/Modes/RenderChat.cs @@ -14,13 +14,12 @@ internal static class RenderChat { internal static void Render(ChatRenderArgs inputOptions) { - FfmpegHandler.DetectFfmpeg(inputOptions.FfmpegPath); + var progress = new CliTaskProgress(); - Progress progress = new(); - progress.ProgressChanged += ProgressHandler.Progress_ProgressChanged; + FfmpegHandler.DetectFfmpeg(inputOptions.FfmpegPath, progress); var renderOptions = GetRenderOptions(inputOptions); - using ChatRenderer chatRenderer = new(renderOptions, progress); + using var chatRenderer = new ChatRenderer(renderOptions, progress); chatRenderer.ParseJsonAsync().Wait(); chatRenderer.RenderVideoAsync(new CancellationToken()).Wait(); } diff --git a/TwitchDownloaderCLI/Modes/UpdateChat.cs b/TwitchDownloaderCLI/Modes/UpdateChat.cs index 926e92e6..f448c906 100644 --- a/TwitchDownloaderCLI/Modes/UpdateChat.cs +++ b/TwitchDownloaderCLI/Modes/UpdateChat.cs @@ -15,11 +15,11 @@ internal static void Update(ChatUpdateArgs inputOptions) { var updateOptions = GetUpdateOptions(inputOptions); - ChatUpdater chatUpdater = new(updateOptions); - Progress progress = new(); - progress.ProgressChanged += ProgressHandler.Progress_ProgressChanged; + var progress = new CliTaskProgress(); + + var chatUpdater = new ChatUpdater(updateOptions, progress); chatUpdater.ParseJsonAsync().Wait(); - chatUpdater.UpdateAsync(progress, new CancellationToken()).Wait(); + chatUpdater.UpdateAsync(new CancellationToken()).Wait(); } private static ChatUpdateOptions GetUpdateOptions(ChatUpdateArgs inputOptions) diff --git a/TwitchDownloaderCLI/Tools/CliTaskProgress.cs b/TwitchDownloaderCLI/Tools/CliTaskProgress.cs new file mode 100644 index 00000000..98dadb04 --- /dev/null +++ b/TwitchDownloaderCLI/Tools/CliTaskProgress.cs @@ -0,0 +1,187 @@ +using System; +using TwitchDownloaderCore.Interfaces; + +namespace TwitchDownloaderCLI.Tools +{ + public class CliTaskProgress : ITaskProgress + { + private const string STATUS_PREAMBLE = "[STATUS] - "; + private const string VERBOSE_LOG_PREAMBLE = "[VERBOSE] - "; + private const string INFO_LOG_PREAMBLE = "[INFO] - "; + private const string WARNING_LOG_PREAMBLE = "[WARNING] - "; + private const string ERROR_LOG_PREAMBLE = "[ERROR] - "; + private const string FFMPEG_LOG_PREAMBLE = " "; + + private string _status; + private bool _statusIsTemplate; + + private bool _lastWriteHadNewLine = true; + private int _lastStatusLength; + private int _lastPercent = -1; + private TimeSpan _lastTime1 = new(-1); + private TimeSpan _lastTime2 = new(-1); + + public CliTaskProgress() + { + // TODO: Take in ITwitchDownloaderArgs to configure log levels + } + + public void SetStatus(string status) + { + lock (this) + { + _status = status; + _statusIsTemplate = false; + + WriteNewLineMessage(STATUS_PREAMBLE, status); + } + } + + public void SetTemplateStatus(string status, int initialPercent) + { + lock (this) + { + _status = status; + _statusIsTemplate = true; + + if (!_lastWriteHadNewLine) + { + Console.WriteLine(); + } + + _lastPercent = -1; // Ensure that the progress report runs + ReportProgress(initialPercent); + } + } + + public void SetTemplateStatus(string status, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2) + { + lock (this) + { + _status = status; + _statusIsTemplate = true; + + if (!_lastWriteHadNewLine) + { + Console.WriteLine(); + } + + _lastPercent = -1; // Ensure that the progress report runs + ReportProgress(initialPercent, initialTime1, initialTime2); + } + } + + public void ReportProgress(int percent) + { + lock (this) + { + if ((!_lastWriteHadNewLine && _lastPercent == percent) + || !_statusIsTemplate) + { + return; + } + + var status = string.Format(_status, percent); + _lastStatusLength = WriteSameLineMessage(STATUS_PREAMBLE, status, _lastStatusLength); + + _lastWriteHadNewLine = false; + _lastPercent = percent; + } + } + + public void ReportProgress(int percent, TimeSpan time1, TimeSpan time2) + { + lock (this) + { + if ((!_lastWriteHadNewLine && _lastPercent == percent && _lastTime1 == time1 && _lastTime2 == time2) + || !_statusIsTemplate) + { + return; + } + + var status = string.Format(_status, percent, time1, time2); + _lastStatusLength = WriteSameLineMessage(STATUS_PREAMBLE, status, _lastStatusLength); + + _lastWriteHadNewLine = false; + _lastPercent = percent; + _lastTime1 = time1; + _lastTime2 = time2; + } + } + + private int WriteSameLineMessage(string preamble, string message, int previousMessageLength) + { + if (!_lastWriteHadNewLine) + { + Console.Write('\r'); + } + + Console.Write(preamble); + Console.Write(message); + + var messageLength = preamble.Length + message.Length; + if (messageLength < previousMessageLength) + { + // Ensure that the previous line is completely overwritten + for (var i = 0; i < previousMessageLength - messageLength; i++) + { + Console.Write(' '); + } + } + + return messageLength; + } + + public void LogVerbose(string logMessage) + { + lock (this) + { + // WriteNewLineMessage(VERBOSE_LOG_PREAMBLE, logMessage); + } + } + + public void LogInfo(string logMessage) + { + lock (this) + { + WriteNewLineMessage(INFO_LOG_PREAMBLE, logMessage); + } + } + + public void LogWarning(string logMessage) + { + lock (this) + { + WriteNewLineMessage(WARNING_LOG_PREAMBLE, logMessage); + } + } + + public void LogError(string logMessage) + { + lock (this) + { + WriteNewLineMessage(ERROR_LOG_PREAMBLE, logMessage); + } + } + + public void LogFfmpeg(string logMessage) + { + lock (this) + { + WriteNewLineMessage(FFMPEG_LOG_PREAMBLE, logMessage); + } + } + + private void WriteNewLineMessage(string preamble, string message) + { + if (!_lastWriteHadNewLine) + { + Console.WriteLine(); + } + + Console.Write(preamble); + Console.WriteLine(message); + _lastWriteHadNewLine = true; + } + } +} \ No newline at end of file diff --git a/TwitchDownloaderCLI/Tools/ProgressHandler.cs b/TwitchDownloaderCLI/Tools/ProgressHandler.cs deleted file mode 100644 index a51991e7..00000000 --- a/TwitchDownloaderCLI/Tools/ProgressHandler.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using TwitchDownloaderCore; - -namespace TwitchDownloaderCLI.Tools -{ - internal static class ProgressHandler - { - private static string _previousMessage = ""; - - internal static void Progress_ProgressChanged(object sender, ProgressReport e) - { - switch (e.ReportType) - { - case ReportType.Log: - ReportLog(e); - break; - case ReportType.NewLineStatus: - ReportNewLineStatus(e); - break; - case ReportType.SameLineStatus: - ReportSameLineStatus(e); - break; - case ReportType.FfmpegLog: - ReportFfmpegLog(e); - break; - } - } - - private static void ReportLog(ProgressReport e) - { - var currentStatus = Environment.NewLine + "[LOG] - " + e.Data + Environment.NewLine; - _previousMessage = currentStatus; - Console.Write(currentStatus); - } - - private static void ReportNewLineStatus(ProgressReport e) - { - var currentStatus = Environment.NewLine + "[STATUS] - " + e.Data; - if (currentStatus != _previousMessage) - { - _previousMessage = currentStatus; - Console.Write(currentStatus); - } - } - - private static void ReportSameLineStatus(ProgressReport e) - { - var currentStatus = "\r[STATUS] - " + e.Data; - if (currentStatus != _previousMessage) - { - // This ensures the previous message is fully overwritten - currentStatus = currentStatus.PadRight(_previousMessage.Length); - - _previousMessage = currentStatus.TrimEnd(); - Console.Write(currentStatus); - } - } - - private static void ReportFfmpegLog(ProgressReport e) - { - var currentStatus = Environment.NewLine + " " + e.Data; - _previousMessage = currentStatus; - Console.Write(currentStatus); - } - } -} diff --git a/TwitchDownloaderCore/ChatDownloader.cs b/TwitchDownloaderCore/ChatDownloader.cs index 33e01b85..113f0a51 100644 --- a/TwitchDownloaderCore/ChatDownloader.cs +++ b/TwitchDownloaderCore/ChatDownloader.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using TwitchDownloaderCore.Chat; +using TwitchDownloaderCore.Interfaces; using TwitchDownloaderCore.Options; using TwitchDownloaderCore.Tools; using TwitchDownloaderCore.TwitchObjects; @@ -18,6 +19,7 @@ namespace TwitchDownloaderCore public sealed class ChatDownloader { private readonly ChatDownloadOptions downloadOptions; + private readonly ITaskProgress _progress; private static readonly HttpClient HttpClient = new() { @@ -31,15 +33,16 @@ private enum DownloadType Video } - public ChatDownloader(ChatDownloadOptions chatDownloadOptions) + public ChatDownloader(ChatDownloadOptions chatDownloadOptions, ITaskProgress progress) { downloadOptions = chatDownloadOptions; + _progress = progress; downloadOptions.TempFolder = Path.Combine( string.IsNullOrWhiteSpace(downloadOptions.TempFolder) ? Path.GetTempPath() : downloadOptions.TempFolder, "TwitchDownloader"); } - private static async Task> DownloadSection(double videoStart, double videoEnd, string videoId, IProgress progress, ChatFormat format, CancellationToken cancellationToken) + private static async Task> DownloadSection(double videoStart, double videoEnd, string videoId, IProgress progress, ChatFormat format, CancellationToken cancellationToken) { var comments = new List(); //GQL only wants ints @@ -118,7 +121,7 @@ private static async Task> DownloadSection(double videoStart, doub if (progress != null) { int percent = (int)Math.Floor((latestMessage - videoStart) / videoDuration * 100); - progress.Report(new ProgressReport() { ReportType = ReportType.Percent, Data = percent }); + progress.Report(percent); } if (isFirst) @@ -241,7 +244,7 @@ private static List ConvertComments(CommentVideo video, ChatFormat form return returnList; } - public async Task DownloadAsync(IProgress progress, CancellationToken cancellationToken) + public async Task DownloadAsync(CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(downloadOptions.Id)) { @@ -357,7 +360,7 @@ public async Task DownloadAsync(IProgress progress, Cancellation chatRoot.video.game = game; videoDuration = videoEnd - videoStart; - var tasks = new List>>(); + var downloadTasks = new List>>(connectionCount); var percentages = new int[connectionCount]; double chunk = videoDuration / connectionCount; @@ -365,47 +368,23 @@ public async Task DownloadAsync(IProgress progress, Cancellation { int tc = i; - Progress taskProgress = null; - if (!downloadOptions.Silent) + var taskProgress = new Progress(percent => { - taskProgress = new Progress(progressReport => - { - if (progressReport.ReportType != ReportType.Percent) - { - progress.Report(progressReport); - } - else - { - var percent = (int)progressReport.Data; - if (percent > 100) - { - percent = 100; - } - - percentages[tc] = percent; - - percent = 0; - for (int j = 0; j < connectionCount; j++) - { - percent += percentages[j]; - } - - percent /= connectionCount; + percentages[tc] = Math.Clamp(percent, 0, 100); - progress.Report(new ProgressReport() { ReportType = ReportType.SameLineStatus, Data = $"Downloading {percent}%" }); - progress.Report(new ProgressReport() { ReportType = ReportType.Percent, Data = percent }); - } - }); - } + var reportPercent = percentages.Sum() / connectionCount; + _progress.ReportProgress(reportPercent); + }); double start = videoStart + chunk * i; - tasks.Add(DownloadSection(start, start + chunk, videoId, taskProgress, downloadOptions.DownloadFormat, cancellationToken)); + downloadTasks.Add(DownloadSection(start, start + chunk, videoId, taskProgress, downloadOptions.DownloadFormat, cancellationToken)); } - await Task.WhenAll(tasks); + _progress.SetTemplateStatus("Downloading {0}%", 0); + await Task.WhenAll(downloadTasks); - var sortedComments = new List(tasks.Count); - foreach (var commentTask in tasks) + var sortedComments = new List(downloadTasks.Count); + foreach (var commentTask in downloadTasks) { sortedComments.AddRange(commentTask.Result); } @@ -416,18 +395,25 @@ public async Task DownloadAsync(IProgress progress, Cancellation if (downloadOptions.EmbedData && (downloadOptions.DownloadFormat is ChatFormat.Json or ChatFormat.Html)) { - progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = "Downloading + Embedding Images" }); + _progress.SetTemplateStatus("Downloading + Embedding Images {0}%", 0); chatRoot.embeddedData = new EmbeddedData(); // This is the exact same process as in ChatUpdater.cs but not in a task oriented manner // TODO: Combine this with ChatUpdater in a different file - List thirdPartyEmotes = await TwitchHelper.GetThirdPartyEmotes(chatRoot.comments, chatRoot.streamer.id, downloadOptions.TempFolder, bttv: downloadOptions.BttvEmotes, ffz: downloadOptions.FfzEmotes, stv: downloadOptions.StvEmotes, progress: progress, cancellationToken: cancellationToken); + List thirdPartyEmotes = await TwitchHelper.GetThirdPartyEmotes(chatRoot.comments, chatRoot.streamer.id, downloadOptions.TempFolder, _progress, bttv: downloadOptions.BttvEmotes, ffz: downloadOptions.FfzEmotes, stv: downloadOptions.StvEmotes, cancellationToken: cancellationToken); + _progress.ReportProgress(50 / 4); List firstPartyEmotes = await TwitchHelper.GetEmotes(chatRoot.comments, downloadOptions.TempFolder, cancellationToken: cancellationToken); + _progress.ReportProgress(50 / 4 * 2); List twitchBadges = await TwitchHelper.GetChatBadges(chatRoot.comments, chatRoot.streamer.id, downloadOptions.TempFolder, cancellationToken: cancellationToken); + _progress.ReportProgress(50 / 4 * 3); List twitchBits = await TwitchHelper.GetBits(chatRoot.comments, downloadOptions.TempFolder, chatRoot.streamer.id.ToString(), cancellationToken: cancellationToken); + _progress.ReportProgress(50); cancellationToken.ThrowIfCancellationRequested(); + var totalImageCount = thirdPartyEmotes.Count + firstPartyEmotes.Count + twitchBadges.Count + twitchBits.Count; + var imagesProcessed = 0; + foreach (TwitchEmote emote in thirdPartyEmotes) { EmbedEmoteData newEmote = new EmbedEmoteData(); @@ -438,7 +424,9 @@ public async Task DownloadAsync(IProgress progress, Cancellation newEmote.width = emote.Width / emote.ImageScale; newEmote.height = emote.Height / emote.ImageScale; chatRoot.embeddedData.thirdParty.Add(newEmote); + _progress.ReportProgress(++imagesProcessed * 100 / totalImageCount + 50); } + foreach (TwitchEmote emote in firstPartyEmotes) { EmbedEmoteData newEmote = new EmbedEmoteData(); @@ -448,14 +436,18 @@ public async Task DownloadAsync(IProgress progress, Cancellation newEmote.width = emote.Width / emote.ImageScale; newEmote.height = emote.Height / emote.ImageScale; chatRoot.embeddedData.firstParty.Add(newEmote); + _progress.ReportProgress(++imagesProcessed * 100 / totalImageCount + 50); } + foreach (ChatBadge badge in twitchBadges) { EmbedChatBadge newBadge = new EmbedChatBadge(); newBadge.name = badge.Name; newBadge.versions = badge.VersionsData; chatRoot.embeddedData.twitchBadges.Add(newBadge); + _progress.ReportProgress(++imagesProcessed * 100 / totalImageCount + 50); } + foreach (CheerEmote bit in twitchBits) { EmbedCheerEmote newBit = new EmbedCheerEmote(); @@ -473,13 +465,14 @@ public async Task DownloadAsync(IProgress progress, Cancellation newBit.tierList.Add(emotePair.Key, newEmote); } chatRoot.embeddedData.twitchBits.Add(newBit); + _progress.ReportProgress(++imagesProcessed * 100 / totalImageCount + 50); } } if (downloadOptions.DownloadFormat is ChatFormat.Json) { //Best effort, but if we fail oh well - progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = "Backfilling commenter info" }); + _progress.SetStatus("Backfilling commenter info"); List userList = chatRoot.comments.DistinctBy(x => x.commenter._id).Select(x => x.commenter._id).ToList(); Dictionary userInfo = new Dictionary(); int batchSize = 100; @@ -500,7 +493,7 @@ public async Task DownloadAsync(IProgress progress, Cancellation if (failedInfo) { - progress.Report(new ProgressReport() { ReportType = ReportType.Log, Data = "Failed to backfill some commenter info" }); + _progress.LogInfo("Failed to backfill some commenter info"); } foreach (var comment in chatRoot.comments) @@ -515,7 +508,7 @@ public async Task DownloadAsync(IProgress progress, Cancellation } } - progress.Report(new ProgressReport(ReportType.NewLineStatus, "Writing output file")); + _progress.SetStatus("Writing output file"); switch (downloadOptions.DownloadFormat) { case ChatFormat.Json: diff --git a/TwitchDownloaderCore/ChatRenderer.cs b/TwitchDownloaderCore/ChatRenderer.cs index acd375f7..cd2a72b3 100644 --- a/TwitchDownloaderCore/ChatRenderer.cs +++ b/TwitchDownloaderCore/ChatRenderer.cs @@ -16,6 +16,7 @@ using System.Threading.Tasks; using TwitchDownloaderCore.Chat; using TwitchDownloaderCore.Extensions; +using TwitchDownloaderCore.Interfaces; using TwitchDownloaderCore.Options; using TwitchDownloaderCore.Tools; using TwitchDownloaderCore.TwitchObjects; @@ -38,7 +39,7 @@ public sealed class ChatRenderer : IDisposable // TODO: Use FrozenDictionary when .NET 8 private static readonly IReadOnlyDictionary AllEmojiSequences = Emoji.All.ToDictionary(e => e.SortOrder, e => e.Sequence.AsString); - private readonly IProgress _progress; + private readonly ITaskProgress _progress; private readonly ChatRenderOptions renderOptions; private List badgeList = new List(); private List emoteList = new List(); @@ -53,7 +54,7 @@ public sealed class ChatRenderer : IDisposable private SKPaint outlinePaint; private readonly HighlightIcons highlightIcons; - public ChatRenderer(ChatRenderOptions chatRenderOptions, IProgress progress = null) + public ChatRenderer(ChatRenderOptions chatRenderOptions, ITaskProgress progress) { renderOptions = chatRenderOptions; renderOptions.TempFolder = Path.Combine( @@ -70,7 +71,7 @@ public ChatRenderer(ChatRenderOptions chatRenderOptions, IProgress FetchScaledImages(cancellationToken), cancellationToken); if (renderOptions.DisperseCommentOffsets) @@ -121,7 +122,7 @@ public async Task RenderVideoAsync(CancellationToken cancellationToken) FfmpegProcess ffmpegProcess = GetFfmpegProcess(0, false); FfmpegProcess maskProcess = renderOptions.GenerateMask ? GetFfmpegProcess(0, true) : null; - _progress.Report(new ProgressReport(ReportType.NewLineStatus, "Rendering Video: 0% [2/2]")); + _progress.SetTemplateStatus(@"Rendering Video {0}% ({1:h\hm\ms\s} Elapsed | {2:h\hm\ms\s} Remaining)", 0, TimeSpan.Zero, TimeSpan.Zero); try { @@ -286,23 +287,20 @@ private static SKTypeface GetInterTypeface(SKFontStyle fontStyle) } } - if (_progress != null && currentTick % 3 == 0) + if (currentTick % 3 == 0) { - double percentDouble = (currentTick - startTick) / (double)(endTick - startTick) * 100.0; - int percentInt = (int)percentDouble; - _progress.Report(new ProgressReport(percentInt)); + var percent = (currentTick - startTick) / (double)(endTick - startTick) * 100; + var elapsed = stopwatch.Elapsed; + var elapsedSeconds = elapsed.TotalSeconds; - int timeLeftInt = (int)(100.0 / percentDouble * stopwatch.Elapsed.TotalSeconds) - (int)stopwatch.Elapsed.TotalSeconds; - TimeSpan timeLeft = new TimeSpan(0, 0, timeLeftInt); - TimeSpan timeElapsed = new TimeSpan(0, 0, (int)stopwatch.Elapsed.TotalSeconds); - _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Rendering Video: {percentInt}% ({timeElapsed:h\\hm\\ms\\s} Elapsed | {timeLeft:h\\hm\\ms\\s} Remaining)")); + var secondsLeft = unchecked((int)(100 / percent * elapsedSeconds - elapsedSeconds)); + _progress.ReportProgress((int)Math.Round(percent), elapsed, TimeSpan.FromSeconds(secondsLeft)); } } stopwatch.Stop(); - _progress?.Report(new ProgressReport(100)); - _progress?.Report(new ProgressReport(ReportType.SameLineStatus, "Rendering Video: 100%")); - _progress?.Report(new ProgressReport(ReportType.Log, $"FINISHED. RENDER TIME: {stopwatch.Elapsed.TotalSeconds:F1}s SPEED: {(endTick - startTick) / (double)renderOptions.Framerate / stopwatch.Elapsed.TotalSeconds:F2}x")); + _progress.ReportProgress(100, stopwatch.Elapsed, TimeSpan.Zero); + _progress.LogInfo($"FINISHED. RENDER TIME: {stopwatch.Elapsed.TotalSeconds:F1}s SPEED: {(endTick - startTick) / (double)renderOptions.Framerate / stopwatch.Elapsed.TotalSeconds:F2}x"); latestUpdate?.Image.Dispose(); @@ -390,7 +388,7 @@ private FfmpegProcess GetFfmpegProcess(int partNumber, bool isMask) { if (e.Data != null) { - _progress.Report(new ProgressReport() { ReportType = ReportType.FfmpegLog, Data = e.Data }); + _progress.LogFfmpeg(e.Data); } }; } @@ -1656,8 +1654,8 @@ private async Task> GetScaledEmotes(CancellationToken cancella private async Task> GetScaledThirdEmotes(CancellationToken cancellationToken) { - var emoteThirdTask = await TwitchHelper.GetThirdPartyEmotes(chatRoot.comments, chatRoot.streamer.id, renderOptions.TempFolder, chatRoot.embeddedData, renderOptions.BttvEmotes, renderOptions.FfzEmotes, - renderOptions.StvEmotes, renderOptions.AllowUnlistedEmotes, renderOptions.Offline, _progress, cancellationToken); + var emoteThirdTask = await TwitchHelper.GetThirdPartyEmotes(chatRoot.comments, chatRoot.streamer.id, renderOptions.TempFolder, _progress, chatRoot.embeddedData, renderOptions.BttvEmotes, renderOptions.FfzEmotes, + renderOptions.StvEmotes, renderOptions.AllowUnlistedEmotes, renderOptions.Offline, cancellationToken); foreach (var emote in emoteThirdTask) { @@ -1746,7 +1744,7 @@ private SKPaint GetFallbackFont(int input) if (!noFallbackFontFound) { noFallbackFontFound = true; - _progress?.Report(new ProgressReport(ReportType.Log, "No valid typefaces were found for some messages.")); + _progress.LogWarning("No valid typefaces were found for some messages."); } } diff --git a/TwitchDownloaderCore/ChatUpdater.cs b/TwitchDownloaderCore/ChatUpdater.cs index 50413e74..3bfaac85 100644 --- a/TwitchDownloaderCore/ChatUpdater.cs +++ b/TwitchDownloaderCore/ChatUpdater.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using TwitchDownloaderCore.Chat; +using TwitchDownloaderCore.Interfaces; using TwitchDownloaderCore.Options; using TwitchDownloaderCore.Tools; using TwitchDownloaderCore.TwitchObjects; @@ -18,16 +19,18 @@ public sealed class ChatUpdater private readonly object _cropChatRootLock = new(); private readonly ChatUpdateOptions _updateOptions; + private readonly ITaskProgress _progress; - public ChatUpdater(ChatUpdateOptions updateOptions) + public ChatUpdater(ChatUpdateOptions updateOptions, ITaskProgress progress) { _updateOptions = updateOptions; + _progress = progress; _updateOptions.TempFolder = Path.Combine( string.IsNullOrWhiteSpace(_updateOptions.TempFolder) ? Path.GetTempPath() : _updateOptions.TempFolder, "TwitchDownloader"); } - public async Task UpdateAsync(IProgress progress, CancellationToken cancellationToken) + public async Task UpdateAsync(CancellationToken cancellationToken) { chatRoot.FileInfo = new() { Version = ChatRootVersion.CurrentVersion, CreatedAt = chatRoot.FileInfo.CreatedAt, UpdatedAt = DateTime.Now }; if (!Path.GetExtension(_updateOptions.InputFile.Replace(".gz", ""))!.Equals(".json", StringComparison.OrdinalIgnoreCase)) @@ -43,13 +46,13 @@ public async Task UpdateAsync(IProgress progress, CancellationTo && (_updateOptions.EmbedMissing || _updateOptions.ReplaceEmbeds)) totalSteps++; currentStep++; - await UpdateVideoInfo(totalSteps, currentStep, progress, cancellationToken); + await UpdateVideoInfo(totalSteps, currentStep, cancellationToken); // If we are editing the chat crop if (_updateOptions.CropBeginning || _updateOptions.CropEnding) { currentStep++; - await UpdateChatCrop(totalSteps, currentStep, progress, cancellationToken); + await UpdateChatCrop(totalSteps, currentStep, cancellationToken); } // If we are updating/replacing embeds @@ -57,12 +60,12 @@ public async Task UpdateAsync(IProgress progress, CancellationTo && (_updateOptions.EmbedMissing || _updateOptions.ReplaceEmbeds)) { currentStep++; - await UpdateEmbeds(currentStep, totalSteps, progress, cancellationToken); + await UpdateEmbeds(currentStep, totalSteps, cancellationToken); } // Finally save the output to file! - progress.Report(new ProgressReport(ReportType.NewLineStatus, $"Writing Output File [{++currentStep}/{totalSteps}]")); - progress.Report(new ProgressReport(currentStep * 100 / totalSteps)); + _progress.SetStatus($"Writing Output File [{++currentStep}/{totalSteps}]"); + _progress.ReportProgress(currentStep * 100 / totalSteps); switch (_updateOptions.OutputFormat) { @@ -80,10 +83,10 @@ public async Task UpdateAsync(IProgress progress, CancellationTo } } - private async Task UpdateVideoInfo(int totalSteps, int currentStep, IProgress progress, CancellationToken cancellationToken) + private async Task UpdateVideoInfo(int totalSteps, int currentStep, CancellationToken cancellationToken) { - progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Updating Video Info [{currentStep}/{totalSteps}]")); - progress.Report(new ProgressReport(currentStep * 100 / totalSteps)); + _progress.SetStatus($"Updating Video Info [{currentStep}/{totalSteps}]"); + _progress.ReportProgress(currentStep * 100 / totalSteps); if (string.IsNullOrWhiteSpace(chatRoot.video.id)) { @@ -102,7 +105,7 @@ private async Task UpdateVideoInfo(int totalSteps, int currentStep, IProgress progress, CancellationToken cancellationToken) + private async Task UpdateChatCrop(int totalSteps, int currentStep, CancellationToken cancellationToken) { - progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Updating Chat Crop [{currentStep}/{totalSteps}]")); - progress.Report(new ProgressReport(currentStep * 100 / totalSteps)); - - bool cropTaskVodExpired = false; - var cropTaskProgress = new Progress(report => - { - if (((string)report.Data).Contains("vod is expired", StringComparison.OrdinalIgnoreCase)) - { - // If the user is moving both crops in one command, we only want to propagate a 'vod expired/id corrupt' report once - if (cropTaskVodExpired) - { - return; - } - - cropTaskVodExpired = true; - } - - progress.Report(report); - }); + _progress.SetStatus($"Updating Chat Crop [{currentStep}/{totalSteps}]"); + _progress.ReportProgress(currentStep * 100 / totalSteps); int inputCommentCount = chatRoot.comments.Count; var chatCropTasks = new[] { - ChatBeginningCropTask(cropTaskProgress, cancellationToken), - ChatEndingCropTask(cropTaskProgress, cancellationToken) + ChatBeginningCropTask(cancellationToken), + ChatEndingCropTask(cancellationToken) }; await Task.WhenAll(chatCropTasks); @@ -228,29 +214,29 @@ private async Task UpdateChatCrop(int totalSteps, int currentStep, IProgress progress, CancellationToken cancellationToken) + private async Task UpdateEmbeds(int currentStep, int totalSteps, CancellationToken cancellationToken) { - progress.Report(new ProgressReport(ReportType.NewLineStatus, $"Updating Embeds [{currentStep}/{totalSteps}]")); - progress.Report(new ProgressReport(currentStep * 100 / totalSteps)); + _progress.SetStatus($"Updating Embeds [{currentStep}/{totalSteps}]"); + _progress.ReportProgress(currentStep * 100 / totalSteps); chatRoot.embeddedData ??= new EmbeddedData(); var embedTasks = new[] { - Task.Run(() => FirstPartyEmoteTask(progress, cancellationToken), cancellationToken), - Task.Run(() => ThirdPartyEmoteTask(progress, cancellationToken), cancellationToken), - Task.Run(() => ChatBadgeTask(progress, cancellationToken), cancellationToken), - Task.Run(() => BitTask(progress, cancellationToken), cancellationToken), + Task.Run(() => FirstPartyEmoteTask(cancellationToken), cancellationToken), + Task.Run(() => ThirdPartyEmoteTask(cancellationToken), cancellationToken), + Task.Run(() => ChatBadgeTask(cancellationToken), cancellationToken), + Task.Run(() => BitTask(cancellationToken), cancellationToken), }; await Task.WhenAll(embedTasks); } - private async Task FirstPartyEmoteTask(IProgress progress = null, CancellationToken cancellationToken = default) + private async Task FirstPartyEmoteTask(CancellationToken cancellationToken = default) { List firstPartyEmoteList = await TwitchHelper.GetEmotes(chatRoot.comments, _updateOptions.TempFolder, _updateOptions.ReplaceEmbeds ? null : chatRoot.embeddedData, cancellationToken: cancellationToken); @@ -266,12 +252,12 @@ private async Task FirstPartyEmoteTask(IProgress progress = null newEmote.height = emote.Height / emote.ImageScale; chatRoot.embeddedData.firstParty.Add(newEmote); } - progress?.Report(new ProgressReport(ReportType.Log, $"Input 1st party emote count: {inputCount}. Output count: {chatRoot.embeddedData.firstParty.Count}")); + _progress.LogInfo($"Input 1st party emote count: {inputCount}. Output count: {chatRoot.embeddedData.firstParty.Count}"); } - private async Task ThirdPartyEmoteTask(IProgress progress = null, CancellationToken cancellationToken = default) + private async Task ThirdPartyEmoteTask(CancellationToken cancellationToken = default) { - List thirdPartyEmoteList = await TwitchHelper.GetThirdPartyEmotes(chatRoot.comments, chatRoot.streamer.id, _updateOptions.TempFolder, _updateOptions.ReplaceEmbeds ? null : chatRoot.embeddedData, _updateOptions.BttvEmotes, _updateOptions.FfzEmotes, _updateOptions.StvEmotes, progress: progress, cancellationToken: cancellationToken); + List thirdPartyEmoteList = await TwitchHelper.GetThirdPartyEmotes(chatRoot.comments, chatRoot.streamer.id, _updateOptions.TempFolder, _progress, _updateOptions.ReplaceEmbeds ? null : chatRoot.embeddedData, _updateOptions.BttvEmotes, _updateOptions.FfzEmotes, _updateOptions.StvEmotes, cancellationToken: cancellationToken); int inputCount = chatRoot.embeddedData.thirdParty.Count; chatRoot.embeddedData.thirdParty = new List(); @@ -286,10 +272,10 @@ private async Task ThirdPartyEmoteTask(IProgress progress = null newEmote.height = emote.Height / emote.ImageScale; chatRoot.embeddedData.thirdParty.Add(newEmote); } - progress?.Report(new ProgressReport(ReportType.Log, $"Input 3rd party emote count: {inputCount}. Output count: {chatRoot.embeddedData.thirdParty.Count}")); + _progress.LogInfo($"Input 3rd party emote count: {inputCount}. Output count: {chatRoot.embeddedData.thirdParty.Count}"); } - private async Task ChatBadgeTask(IProgress progress = null, CancellationToken cancellationToken = default) + private async Task ChatBadgeTask(CancellationToken cancellationToken = default) { List badgeList = await TwitchHelper.GetChatBadges(chatRoot.comments, chatRoot.streamer.id, _updateOptions.TempFolder, _updateOptions.ReplaceEmbeds ? null : chatRoot.embeddedData, cancellationToken: cancellationToken); @@ -302,10 +288,10 @@ private async Task ChatBadgeTask(IProgress progress = null, Canc newBadge.versions = badge.VersionsData; chatRoot.embeddedData.twitchBadges.Add(newBadge); } - progress?.Report(new ProgressReport(ReportType.Log, $"Input badge count: {inputCount}. Output count: {chatRoot.embeddedData.twitchBadges.Count}")); + _progress.LogInfo($"Input badge count: {inputCount}. Output count: {chatRoot.embeddedData.twitchBadges.Count}"); } - private async Task BitTask(IProgress progress = null, CancellationToken cancellationToken = default) + private async Task BitTask(CancellationToken cancellationToken = default) { List bitList = await TwitchHelper.GetBits(chatRoot.comments, _updateOptions.TempFolder, chatRoot.streamer.id.ToString(), _updateOptions.ReplaceEmbeds ? null : chatRoot.embeddedData, cancellationToken: cancellationToken); @@ -329,10 +315,12 @@ private async Task BitTask(IProgress progress = null, Cancellati } chatRoot.embeddedData.twitchBits.Add(newBit); } - progress?.Report(new ProgressReport(ReportType.Log, $"Input bit emote count: {inputCount}. Output count: {chatRoot.embeddedData.twitchBits.Count}")); + _progress.LogInfo($"Input bit emote count: {inputCount}. Output count: {chatRoot.embeddedData.twitchBits.Count}"); } - private async Task ChatBeginningCropTask(IProgress progress, CancellationToken cancellationToken) + private bool _cropTaskReportedExpiredVod; + + private async Task ChatBeginningCropTask(CancellationToken cancellationToken) { if (!_updateOptions.CropBeginning) { @@ -352,7 +340,11 @@ private async Task ChatBeginningCropTask(IProgress progress, Can } catch (NullReferenceException) { - progress.Report(new ProgressReport(ReportType.Log, "Unable to fetch possible missing comments: source VOD is expired or embedded ID is corrupt")); + if (!_cropTaskReportedExpiredVod) + { + _cropTaskReportedExpiredVod = true; + _progress.LogInfo("Unable to fetch possible missing comments: source VOD is expired or embedded ID is corrupt"); + } } if (File.Exists(tempFile)) @@ -365,7 +357,7 @@ private async Task ChatBeginningCropTask(IProgress progress, Can chatRoot.video.start = Math.Min(Math.Max(_updateOptions.CropBeginningTime, 0.0), beginningCropClamp); } - private async Task ChatEndingCropTask(IProgress progress, CancellationToken cancellationToken) + private async Task ChatEndingCropTask(CancellationToken cancellationToken) { if (!_updateOptions.CropEnding) { @@ -385,7 +377,11 @@ private async Task ChatEndingCropTask(IProgress progress, Cancel } catch (NullReferenceException) { - progress.Report(new ProgressReport(ReportType.Log, "Unable to fetch possible missing comments: source VOD is expired or embedded ID is corrupt")); + if (!_cropTaskReportedExpiredVod) + { + _cropTaskReportedExpiredVod = true; + _progress.LogInfo("Unable to fetch possible missing comments: source VOD is expired or embedded ID is corrupt"); + } } if (File.Exists(tempFile)) @@ -400,8 +396,8 @@ private async Task ChatEndingCropTask(IProgress progress, Cancel private async Task AppendCommentSection(ChatDownloadOptions downloadOptions, string inputFile, CancellationToken cancellationToken = new()) { - ChatDownloader chatDownloader = new ChatDownloader(downloadOptions); - await chatDownloader.DownloadAsync(new Progress(), cancellationToken); + var chatDownloader = new ChatDownloader(downloadOptions, StubTaskProgress.Instance); + await chatDownloader.DownloadAsync(cancellationToken); ChatRoot newChatRoot = await ChatJson.DeserializeAsync(inputFile, getComments: true, onlyFirstAndLastComments: false, getEmbeds: false, cancellationToken); diff --git a/TwitchDownloaderCore/ClipDownloader.cs b/TwitchDownloaderCore/ClipDownloader.cs index 428a990b..b5438b8d 100644 --- a/TwitchDownloaderCore/ClipDownloader.cs +++ b/TwitchDownloaderCore/ClipDownloader.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using System.Web; using TwitchDownloaderCore.Extensions; +using TwitchDownloaderCore.Interfaces; using TwitchDownloaderCore.Options; using TwitchDownloaderCore.Tools; using TwitchDownloaderCore.TwitchObjects.Gql; @@ -16,10 +17,10 @@ namespace TwitchDownloaderCore public sealed class ClipDownloader { private readonly ClipDownloadOptions downloadOptions; - private readonly IProgress _progress; + private readonly ITaskProgress _progress; private static readonly HttpClient HttpClient = new(); - public ClipDownloader(ClipDownloadOptions clipDownloadOptions, IProgress progress) + public ClipDownloader(ClipDownloadOptions clipDownloadOptions, ITaskProgress progress) { downloadOptions = clipDownloadOptions; _progress = progress; @@ -30,7 +31,7 @@ public ClipDownloader(ClipDownloadOptions clipDownloadOptions, IProgress(DownloadProgressHandler), cancellationToken); - _progress.Report(new ProgressReport(ReportType.NewLineStatus, "Encoding Clip Metadata 0%")); - _progress.Report(new ProgressReport(0)); + _progress.SetTemplateStatus("Encoding Clip Metadata {0}%", 0); var clipChapter = TwitchHelper.GenerateClipChapter(clipInfo.data.clip); await EncodeClipWithMetadata(tempFile, downloadOptions.Filename, clipInfo.data.clip, clipChapter, cancellationToken); @@ -77,11 +76,10 @@ void DownloadProgressHandler(StreamCopyProgress streamProgress) if (!File.Exists(downloadOptions.Filename)) { File.Move(tempFile, downloadOptions.Filename); - _progress.Report(new ProgressReport(ReportType.Log, "Unable to serialize metadata. The download has been completed without custom metadata.")); + _progress.LogError("Unable to serialize metadata. The download has been completed without custom metadata."); } - _progress.Report(new ProgressReport(ReportType.SameLineStatus, "Encoding Clip Metadata 100%")); - _progress.Report(new ProgressReport(100)); + _progress.ReportProgress(100); } finally { diff --git a/TwitchDownloaderCore/Interfaces/ITaskLogger.cs b/TwitchDownloaderCore/Interfaces/ITaskLogger.cs new file mode 100644 index 00000000..c04032f3 --- /dev/null +++ b/TwitchDownloaderCore/Interfaces/ITaskLogger.cs @@ -0,0 +1,12 @@ +namespace TwitchDownloaderCore.Interfaces +{ + public interface ITaskLogger + { + // TODO: Add DefaultInterpolatedStringHandler overloads once log levels are implemented for zero-alloc logging + void LogVerbose(string logMessage); + void LogInfo(string logMessage); + void LogWarning(string logMessage); + void LogError(string logMessage); + void LogFfmpeg(string logMessage); + } +} \ No newline at end of file diff --git a/TwitchDownloaderCore/Interfaces/ITaskProgress.cs b/TwitchDownloaderCore/Interfaces/ITaskProgress.cs new file mode 100644 index 00000000..a14460e4 --- /dev/null +++ b/TwitchDownloaderCore/Interfaces/ITaskProgress.cs @@ -0,0 +1,14 @@ +using System; + +namespace TwitchDownloaderCore.Interfaces +{ + // TODO: Add StringSyntaxAttributes when .NET 7+ + public interface ITaskProgress : ITaskLogger + { + void SetStatus(string status); + void SetTemplateStatus(string status, int initialPercent); + void SetTemplateStatus(string status, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2); + void ReportProgress(int percent); + void ReportProgress(int percent, TimeSpan time1, TimeSpan time2); + } +} \ No newline at end of file diff --git a/TwitchDownloaderCore/ProgressReport.cs b/TwitchDownloaderCore/ProgressReport.cs deleted file mode 100644 index 75d22e7b..00000000 --- a/TwitchDownloaderCore/ProgressReport.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace TwitchDownloaderCore -{ - public enum ReportType - { - Log, - Percent, - NewLineStatus, - SameLineStatus, - FfmpegLog - } - - public class ProgressReport - { - public ReportType ReportType { get; set; } - public object Data { get; set; } - - public ProgressReport() { } - - public ProgressReport(int percent) - { - ReportType = ReportType.Percent; - Data = percent; - } - - public ProgressReport(ReportType reportType, string message) - { - ReportType = reportType; - Data = message; - } - } -} \ No newline at end of file diff --git a/TwitchDownloaderCore/Tools/DriveHelper.cs b/TwitchDownloaderCore/Tools/DriveHelper.cs index 299cbc5a..743dfd8f 100644 --- a/TwitchDownloaderCore/Tools/DriveHelper.cs +++ b/TwitchDownloaderCore/Tools/DriveHelper.cs @@ -1,7 +1,7 @@ -using System; -using System.IO; +using System.IO; using System.Threading; using System.Threading.Tasks; +using TwitchDownloaderCore.Interfaces; namespace TwitchDownloaderCore.Tools { @@ -29,12 +29,12 @@ public static DriveInfo GetOutputDrive(string outputPath) return outputDrive; } - public static async Task WaitForDrive(DriveInfo drive, IProgress progress, CancellationToken cancellationToken) + public static async Task WaitForDrive(DriveInfo drive, ITaskLogger logger, CancellationToken cancellationToken) { var driveNotReadyCount = 0; while (!drive.IsReady) { - progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Waiting for output drive ({(driveNotReadyCount + 1) / 2f:F1}s)")); + logger.LogInfo($"Waiting for output drive ({(driveNotReadyCount + 1) / 2f:F1}s)"); await Task.Delay(500, cancellationToken); if (++driveNotReadyCount >= 20) diff --git a/TwitchDownloaderCore/Tools/StubTaskProgress.cs b/TwitchDownloaderCore/Tools/StubTaskProgress.cs new file mode 100644 index 00000000..854cc870 --- /dev/null +++ b/TwitchDownloaderCore/Tools/StubTaskProgress.cs @@ -0,0 +1,32 @@ +using System; +using TwitchDownloaderCore.Interfaces; + +namespace TwitchDownloaderCore.Tools +{ + public class StubTaskProgress : ITaskProgress + { + public static readonly StubTaskProgress Instance = new(); + + private StubTaskProgress() { } + + public void LogVerbose(string logMessage) { } + + public void LogInfo(string logMessage) { } + + public void LogWarning(string logMessage) { } + + public void LogError(string logMessage) { } + + public void LogFfmpeg(string logMessage) { } + + public void SetStatus(string status) { } + + public void SetTemplateStatus(string status, int initialPercent) { } + + public void SetTemplateStatus(string status, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2) { } + + public void ReportProgress(int percent) { } + + public void ReportProgress(int percent, TimeSpan time1, TimeSpan time2) { } + } +} \ No newline at end of file diff --git a/TwitchDownloaderCore/TsMerger.cs b/TwitchDownloaderCore/TsMerger.cs index e3e54128..16d01f6d 100644 --- a/TwitchDownloaderCore/TsMerger.cs +++ b/TwitchDownloaderCore/TsMerger.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using TwitchDownloaderCore.Interfaces; using TwitchDownloaderCore.Options; using TwitchDownloaderCore.Tools; @@ -11,9 +11,9 @@ namespace TwitchDownloaderCore public sealed class TsMerger { private readonly TsMergeOptions mergeOptions; - private readonly IProgress _progress; + private readonly ITaskProgress _progress; - public TsMerger(TsMergeOptions tsMergeOptions, IProgress progress) + public TsMerger(TsMergeOptions tsMergeOptions, ITaskProgress progress) { mergeOptions = tsMergeOptions; _progress = progress; @@ -48,15 +48,15 @@ public async Task MergeAsync(CancellationToken cancellationToken) } } - _progress.Report(new ProgressReport(ReportType.SameLineStatus, "Verifying Parts 0% [1/2]")); + _progress.SetTemplateStatus("Verifying Parts {0}% [1/2]", 0); await VerifyVideoParts(fileList, cancellationToken); - _progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = "Combining Parts 0% [2/2]" }); + _progress.SetTemplateStatus("Combining Parts {0}% [2/2]", 0); await CombineVideoParts(fileList, cancellationToken); - _progress.Report(new ProgressReport(100)); + _progress.ReportProgress(100); } private async Task VerifyVideoParts(IReadOnlyCollection fileList, CancellationToken cancellationToken) @@ -75,8 +75,7 @@ private async Task VerifyVideoParts(IReadOnlyCollection fileList, Cancel doneCount++; var percent = (int)(doneCount / (double)partCount * 100); - _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Verifying Parts {percent}% [1/2]")); - _progress.Report(new ProgressReport(percent)); + _progress.ReportProgress(percent); cancellationToken.ThrowIfCancellationRequested(); } @@ -89,7 +88,7 @@ private async Task VerifyVideoParts(IReadOnlyCollection fileList, Cancel return; } - _progress.Report(new ProgressReport(ReportType.Log, $"The following TS files are invalid or corrupted: {string.Join(", ", failedParts)}")); + _progress.LogInfo($"The following TS files are invalid or corrupted: {string.Join(", ", failedParts)}"); } } @@ -132,8 +131,7 @@ private async Task CombineVideoParts(IReadOnlyCollection fileList, Cance doneCount++; int percent = (int)(doneCount / (double)partCount * 100); - _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Combining Parts {percent}% [2/2]")); - _progress.Report(new ProgressReport(percent)); + _progress.ReportProgress(percent); cancellationToken.ThrowIfCancellationRequested(); } diff --git a/TwitchDownloaderCore/TwitchHelper.cs b/TwitchDownloaderCore/TwitchHelper.cs index 34d29197..ac63d99f 100644 --- a/TwitchDownloaderCore/TwitchHelper.cs +++ b/TwitchDownloaderCore/TwitchHelper.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Threading.Tasks; using TwitchDownloaderCore.Chat; +using TwitchDownloaderCore.Interfaces; using TwitchDownloaderCore.Tools; using TwitchDownloaderCore.TwitchObjects; using TwitchDownloaderCore.TwitchObjects.Api; @@ -344,7 +345,7 @@ private static async Task> GetStvEmotesMetadata(int stre return returnList; } - public static async Task> GetThirdPartyEmotes(List comments, int streamerId, string cacheFolder, EmbeddedData embeddedData = null, bool bttv = true, bool ffz = true, bool stv = true, bool allowUnlistedEmotes = true, bool offline = false, IProgress progress = null, CancellationToken cancellationToken = default) + public static async Task> GetThirdPartyEmotes(List comments, int streamerId, string cacheFolder, ITaskLogger logger, EmbeddedData embeddedData = null, bool bttv = true, bool ffz = true, bool stv = true, bool allowUnlistedEmotes = true, bool offline = false, CancellationToken cancellationToken = default) { List returnList = new List(); List alreadyAdded = new List(); @@ -390,12 +391,7 @@ public static async Task> GetThirdPartyEmotes(List co } catch (HttpRequestException e) { - if (progress is null) - { - throw new Exception($"BTTV returned HTTP {e.StatusCode}. See inner exception.", e); - } - - progress.Report(new ProgressReport(ReportType.Log, $"BetterTTV returned HTTP {e.StatusCode}. BTTV emotes may not be present for this session.")); + logger.LogError($"BetterTTV returned HTTP {e.StatusCode}. BTTV emotes may not be present for this session."); } } @@ -409,12 +405,7 @@ public static async Task> GetThirdPartyEmotes(List co } catch (HttpRequestException e) { - if (progress is null) - { - throw new Exception($"FFZ returned HTTP {e.StatusCode}. See inner exception.", e); - } - - progress.Report(new ProgressReport(ReportType.Log, $"FFZ returned HTTP {e.StatusCode}. FFZ emotes may not be present for this session.")); + logger.LogError($"FFZ returned HTTP {e.StatusCode}. FFZ emotes may not be present for this session."); } } @@ -428,12 +419,7 @@ public static async Task> GetThirdPartyEmotes(List co } catch (HttpRequestException e) { - if (progress is null) - { - throw new Exception($"7TV returned HTTP {e.StatusCode}. See inner exception.", e); - } - - progress.Report(new ProgressReport(ReportType.Log, $"7TV returned HTTP {e.StatusCode}. 7TV emotes may not be present for this session.")); + logger.LogError($"7TV returned HTTP {e.StatusCode}. 7TV emotes may not be present for this session."); } } @@ -660,7 +646,7 @@ public static async Task> GetChatBadges(List comments, return returnList; } - public static async Task> GetEmojis(string cacheFolder, EmojiVendor emojiVendor, IProgress progress, CancellationToken cancellationToken = default) + public static async Task> GetEmojis(string cacheFolder, EmojiVendor emojiVendor, ITaskLogger logger, CancellationToken cancellationToken = default) { var returnCache = new Dictionary(); @@ -718,6 +704,7 @@ public static async Task> GetEmojis(string cacheFol } } + var failedToDecode = 0; foreach (var emojiPath in emojiFiles) { await using var fs = File.OpenRead(emojiPath); @@ -725,13 +712,19 @@ public static async Task> GetEmojis(string cacheFol if (emojiImage is null) { - progress.Report(new ProgressReport(ReportType.Log, $"Failed to decode emoji {Path.GetFileName(emojiPath)}, skipping.")); + failedToDecode++; + logger.LogVerbose($"Failed to decode emoji {Path.GetFileName(emojiPath)}, skipping."); continue; } returnCache.Add(Path.GetFileNameWithoutExtension(emojiPath), emojiImage); } + if (failedToDecode > 0) + { + logger.LogWarning($"{failedToDecode} emojis failed to decode."); + } + return returnCache; } @@ -857,7 +850,7 @@ public static void SetDirectoryPermissions(string path) /// /// Cleans up any unmanaged cache files from previous runs that were interrupted before cleaning up /// - public static async Task CleanupAbandonedVideoCaches(string cacheFolder, Func itemsToDeleteCallback, IProgress progress) + public static async Task CleanupAbandonedVideoCaches(string cacheFolder, Func itemsToDeleteCallback, ITaskLogger logger) { if (!Directory.Exists(cacheFolder)) { @@ -906,9 +899,9 @@ where DateTime.UtcNow.Ticks - directoryInfo.LastWriteTimeUtc.Ticks > TimeSpan.Ti } } - progress.Report(toDelete.Length == wasDeleted - ? new ProgressReport(ReportType.Log, $"{wasDeleted} old video caches were deleted.") - : new ProgressReport(ReportType.Log, $"{wasDeleted} old video caches were deleted, {toDelete.Length - wasDeleted} could not be deleted.")); + logger.LogInfo(toDelete.Length == wasDeleted + ? $"{wasDeleted} old video caches were deleted." + : $"{wasDeleted} old video caches were deleted, {toDelete.Length - wasDeleted} could not be deleted."); } public static int TimestampToSeconds(string input) diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs index 9dbdc6ed..bcceab4f 100644 --- a/TwitchDownloaderCore/VideoDownloader.cs +++ b/TwitchDownloaderCore/VideoDownloader.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using TwitchDownloaderCore.Extensions; +using TwitchDownloaderCore.Interfaces; using TwitchDownloaderCore.Options; using TwitchDownloaderCore.Tools; using TwitchDownloaderCore.TwitchObjects.Gql; @@ -22,10 +23,10 @@ public sealed class VideoDownloader { private readonly VideoDownloadOptions downloadOptions; private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(30) }; - private readonly IProgress _progress; + private readonly ITaskProgress _progress; private bool _shouldClearCache = true; - public VideoDownloader(VideoDownloadOptions videoDownloadOptions, IProgress progress) + public VideoDownloader(VideoDownloadOptions videoDownloadOptions, ITaskProgress progress = default) { downloadOptions = videoDownloadOptions; downloadOptions.TempFolder = Path.Combine( @@ -44,7 +45,7 @@ public async Task DownloadAsync(CancellationToken cancellationToken) downloadOptions.TempFolder, $"{downloadOptions.Id}_{DateTimeOffset.UtcNow.Ticks}"); - _progress.Report(new ProgressReport(ReportType.SameLineStatus, "Fetching Video Info [1/5]")); + _progress.SetStatus("Fetching Video Info [1/5]"); try { @@ -72,19 +73,19 @@ public async Task DownloadAsync(CancellationToken cancellationToken) Directory.Delete(downloadFolder, true); TwitchHelper.CreateDirectory(downloadFolder); - _progress.Report(new ProgressReport(ReportType.NewLineStatus, "Downloading 0% [2/5]")); + _progress.SetTemplateStatus("Downloading {0}% [2/5]", 0); await DownloadVideoPartsAsync(playlist.Streams, videoListCrop, baseUrl, downloadFolder, vodAge, cancellationToken); - _progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = "Verifying Parts 0% [3/5]" }); + _progress.SetTemplateStatus("Verifying Parts {0}% [3/5]", 0); await VerifyDownloadedParts(playlist.Streams, videoListCrop, baseUrl, downloadFolder, vodAge, cancellationToken); - _progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = "Combining Parts 0% [4/5]" }); + _progress.SetTemplateStatus("Combining Parts {0}% [4/5]", 0); await CombineVideoParts(downloadFolder, playlist.Streams, videoListCrop, cancellationToken); - _progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = "Finalizing Video 0% [5/5]" }); + _progress.SetTemplateStatus("Finalizing Video {0}% [5/5]", 0); var startOffset = TimeSpan.FromSeconds((double)playlist.Streams .Take(videoListCrop.Start.Value) @@ -112,7 +113,7 @@ await FfmpegMetadata.SerializeAsync(metadataPath, videoInfo.owner.displayName, d ffmpegExitCode = await Task.Run(() => RunFfmpegVideoCopy(downloadFolder, metadataPath, startOffset, seekDuration > TimeSpan.Zero ? seekDuration : videoLength), cancellationToken); if (ffmpegExitCode != 0) { - _progress.Report(new ProgressReport(ReportType.Log, $"Failed to finalize video (code {ffmpegExitCode}), retrying in 10 seconds...")); + _progress.LogError($"Failed to finalize video (code {ffmpegExitCode}), retrying in 10 seconds..."); await Task.Delay(10_000, cancellationToken); } } while (ffmpegExitCode != 0 && ffmpegRetries++ < 1); @@ -123,8 +124,7 @@ await FfmpegMetadata.SerializeAsync(metadataPath, videoInfo.owner.displayName, d throw new Exception($"Failed to finalize video. The download cache has not been cleared and can be found at {downloadFolder} along with a log file."); } - _progress.Report(new ProgressReport(ReportType.SameLineStatus, "Finalizing Video 100% [5/5]")); - _progress.Report(new ProgressReport(100)); + _progress.ReportProgress(100); } finally { @@ -147,7 +147,7 @@ private void CheckAvailableStorageSpace(int bandwidth, TimeSpan videoLength) { if (tempFolderDrive.AvailableFreeSpace < videoSizeInBytes * 2) { - _progress.Report(new ProgressReport(ReportType.Log, $"The drive '{tempFolderDrive.Name}' may not have enough free space to complete the download.")); + _progress.LogWarning($"The drive '{tempFolderDrive.Name}' may not have enough free space to complete the download."); } } else @@ -155,12 +155,12 @@ private void CheckAvailableStorageSpace(int bandwidth, TimeSpan videoLength) if (tempFolderDrive.AvailableFreeSpace < videoSizeInBytes) { // More drive space is needed by the raw ts files due to repeat metadata, but the amount of metadata packets can vary between files so we won't bother. - _progress.Report(new ProgressReport(ReportType.Log, $"The drive '{tempFolderDrive.Name}' may not have enough free space to complete the download.")); + _progress.LogWarning($"The drive '{tempFolderDrive.Name}' may not have enough free space to complete the download."); } if (destinationDrive.AvailableFreeSpace < videoSizeInBytes) { - _progress.Report(new ProgressReport(ReportType.Log, $"The drive '{destinationDrive.Name}' may not have enough free space to complete finalization.")); + _progress.LogWarning($"The drive '{destinationDrive.Name}' may not have enough free space to complete finalization."); } } } @@ -250,8 +250,7 @@ private async Task> WaitForDownloadThreads(Task[] { previousDoneCount = videoPartsQueue.Count; var percent = (int)((partCount - previousDoneCount) / (double)partCount * 100); - _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Downloading {percent}% [2/5]")); - _progress.Report(new ProgressReport(percent)); + _progress.ReportProgress(percent); } allThreadsExited = true; @@ -276,9 +275,11 @@ private async Task> WaitForDownloadThreads(Task[] } } - await Task.Delay(300, cancellationToken); + await Task.Delay(100, cancellationToken); } while (!allThreadsExited); + _progress.ReportProgress(100); + if (restartedThreads == maxRestartedThreads) { throw new AggregateException("The download thread restart limit was reached.", downloadExceptions.Values); @@ -321,7 +322,7 @@ private void LogDownloadThreadExceptions(IReadOnlyCollection download } sb.Replace(",", $"{downloadExceptions.Count} errors were encountered while downloading:", 0, 1); - _progress.Report(new ProgressReport(ReportType.Log, sb.ToString())); + _progress.LogInfo(sb.ToString()); } private async Task VerifyDownloadedParts(ICollection playlist, Range videoListCrop, Uri baseUrl, string downloadFolder, double vodAge, CancellationToken cancellationToken) @@ -340,8 +341,7 @@ private async Task VerifyDownloadedParts(ICollection playlist, Rang doneCount++; var percent = (int)(doneCount / (double)partCount * 100); - _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Verifying Parts {percent}% [3/5]")); - _progress.Report(new ProgressReport(percent)); + _progress.ReportProgress(percent); cancellationToken.ThrowIfCancellationRequested(); } @@ -361,7 +361,7 @@ private async Task VerifyDownloadedParts(ICollection playlist, Rang throw new Exception($"Too many parts are corrupted or missing ({failedParts}/{partCount}), aborting."); } - _progress.Report(new ProgressReport(ReportType.Log, $"The following parts will be redownloaded: {string.Join(", ", failedParts)}")); + _progress.LogInfo($"The following parts will be redownloaded: {string.Join(", ", failedParts)}"); await DownloadVideoPartsAsync(failedParts, videoListCrop, baseUrl, downloadFolder, vodAge, cancellationToken); } } @@ -413,7 +413,7 @@ private int RunFfmpegVideoCopy(string downloadFolder, string metadataPath, TimeS logQueue.Enqueue(e.Data); // We cannot use -report ffmpeg arg because it redirects stderr - HandleFfmpegOutput(e.Data, encodingTimeRegex, seekDuration, _progress); + HandleFfmpegOutput(e.Data, encodingTimeRegex, seekDuration); }; process.Start(); @@ -422,7 +422,7 @@ private int RunFfmpegVideoCopy(string downloadFolder, string metadataPath, TimeS using var logWriter = File.AppendText(Path.Combine(downloadFolder, "ffmpegLog.txt")); do // We cannot handle logging inside the ErrorDataReceived lambda because more than 1 can come in at once and cause a race condition. lay295#598 { - Thread.Sleep(330); + Thread.Sleep(100); while (!logQueue.IsEmpty && logQueue.TryDequeue(out var logMessage)) { logWriter.WriteLine(logMessage); @@ -432,9 +432,7 @@ private int RunFfmpegVideoCopy(string downloadFolder, string metadataPath, TimeS return process.ExitCode; } - private bool _reportedPercentIssue; - - private void HandleFfmpegOutput(string output, Regex encodingTimeRegex, TimeSpan videoLength, IProgress progress) + private void HandleFfmpegOutput(string output, Regex encodingTimeRegex, TimeSpan videoLength) { var encodingTimeMatch = encodingTimeRegex.Match(output); if (!encodingTimeMatch.Success) @@ -449,18 +447,7 @@ private void HandleFfmpegOutput(string output, Regex encodingTimeRegex, TimeSpan var percent = (int)Math.Round(encodingTime / videoLength * 100); - if (percent is < 0 or > 100 && !_reportedPercentIssue) - { - _reportedPercentIssue = true; - - // This should no longer occur, but just in case it does... - progress.Report(new ProgressReport(ReportType.Log, - $"{nameof(percent)} was < 0 or > 100 ({percent}). {nameof(output)}: '{output}'. {nameof(videoLength)}: '{videoLength}'. {nameof(encodingTime)}: '{encodingTime}'. " + - $"Please report this as a bug: https://github.com/lay295/TwitchDownloader/issues/new/choose")); - } - - progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Finalizing Video {percent}% [5/5]")); - progress.Report(new ProgressReport(percent)); + _progress.ReportProgress(Math.Clamp(percent, 0, 100)); } /// The may be canceled by this method. @@ -681,8 +668,7 @@ private async Task CombineVideoParts(string downloadFolder, IEnumerable + statusProgressBar.Value = percent + ); + } + + private void SetStatus(string message) + { + Dispatcher.BeginInvoke(() => + statusMessage.Text = message + ); + } + private void AppendLog(string message) { textLog.Dispatcher.BeginInvoke(() => @@ -250,22 +263,6 @@ public ChatDownloadOptions GetOptions(string filename) return options; } - private void OnProgressChanged(ProgressReport progress) - { - switch (progress.ReportType) - { - case ReportType.Percent: - statusProgressBar.Value = (int)progress.Data; - break; - case ReportType.NewLineStatus or ReportType.SameLineStatus: - statusMessage.Text = (string)progress.Data; - break; - case ReportType.Log: - AppendLog((string)progress.Data); - break; - } - } - public void SetImage(string imageUri, bool isGif) { var image = new BitmapImage(); @@ -506,32 +503,31 @@ private async void SplitBtnDownload_Click(object sender, RoutedEventArgs e) else if (radioTimestampNone.IsChecked == true) downloadOptions.TimeFormat = TimestampFormat.None; - ChatDownloader currentDownload = new ChatDownloader(downloadOptions); + var downloadProgress = new WpfTaskProgress(SetPercent, SetStatus, AppendLog); + var currentDownload = new ChatDownloader(downloadOptions, downloadProgress); btnGetInfo.IsEnabled = false; SetEnabled(false, false); SetImage("Images/ppOverheat.gif", true); - statusMessage.Text = Translations.Strings.StatusDone; + statusMessage.Text = Translations.Strings.StatusDownloading; _cancellationTokenSource = new CancellationTokenSource(); UpdateActionButtons(true); - Progress downloadProgress = new Progress(OnProgressChanged); - try { - await currentDownload.DownloadAsync(downloadProgress, _cancellationTokenSource.Token); - statusMessage.Text = Translations.Strings.StatusDone; + await currentDownload.DownloadAsync(_cancellationTokenSource.Token); + downloadProgress.SetStatus(Translations.Strings.StatusDone); SetImage("Images/ppHop.gif", true); } catch (Exception ex) when (ex is OperationCanceledException or TaskCanceledException && _cancellationTokenSource.IsCancellationRequested) { - statusMessage.Text = Translations.Strings.StatusCanceled; + downloadProgress.SetStatus(Translations.Strings.StatusCanceled); SetImage("Images/ppHop.gif", true); } catch (Exception ex) { - statusMessage.Text = Translations.Strings.StatusError; + downloadProgress.SetStatus(Translations.Strings.StatusError); SetImage("Images/peepoSad.png", false); AppendLog(Translations.Strings.ErrorLog + ex.Message); if (Settings.Default.VerboseErrors) @@ -540,7 +536,7 @@ private async void SplitBtnDownload_Click(object sender, RoutedEventArgs e) } } btnGetInfo.IsEnabled = true; - statusProgressBar.Value = 0; + downloadProgress.ReportProgress(0); _cancellationTokenSource.Dispose(); UpdateActionButtons(false); diff --git a/TwitchDownloaderWPF/PageChatRender.xaml.cs b/TwitchDownloaderWPF/PageChatRender.xaml.cs index df7a13a6..56ad929f 100644 --- a/TwitchDownloaderWPF/PageChatRender.xaml.cs +++ b/TwitchDownloaderWPF/PageChatRender.xaml.cs @@ -19,6 +19,7 @@ using TwitchDownloaderCore.Options; using TwitchDownloaderCore.TwitchObjects; using TwitchDownloaderWPF.Properties; +using TwitchDownloaderWPF.Utils; using WpfAnimatedGif; using MessageBox = System.Windows.MessageBox; @@ -150,25 +151,6 @@ public ChatRenderOptions GetOptions(string filename) return options; } - private void OnProgressChanged(ProgressReport progress) - { - switch (progress.ReportType) - { - case ReportType.Percent: - statusProgressBar.Value = (int)progress.Data; - break; - case ReportType.NewLineStatus or ReportType.SameLineStatus: - statusMessage.Text = (string)progress.Data; - break; - case ReportType.Log: - AppendLog((string)progress.Data); - break; - case ReportType.FfmpegLog: - ffmpegLog.Add((string)progress.Data); - break; - } - } - private void LoadSettings() { try @@ -423,6 +405,20 @@ private bool ValidateInputs() return true; } + private void SetPercent(int percent) + { + Dispatcher.BeginInvoke(() => + statusProgressBar.Value = percent + ); + } + + private void SetStatus(string message) + { + Dispatcher.BeginInvoke(() => + statusMessage.Text = message + ); + } + private void AppendLog(string message) { textLog.Dispatcher.BeginInvoke(() => @@ -602,7 +598,7 @@ private async void SplitBtnRender_Click(object sender, RoutedEventArgs e) ChatRenderOptions options = GetOptions(saveFileDialog.FileName); - Progress renderProgress = new Progress(OnProgressChanged); + var renderProgress = new WpfTaskProgress(SetPercent, SetStatus, AppendLog, s => ffmpegLog.Add(s)); ChatRenderer currentRender = new ChatRenderer(options, renderProgress); try { @@ -650,17 +646,17 @@ private async void SplitBtnRender_Click(object sender, RoutedEventArgs e) try { await currentRender.RenderVideoAsync(_cancellationTokenSource.Token); - statusMessage.Text = Translations.Strings.StatusDone; + renderProgress.SetStatus(Translations.Strings.StatusDone); SetImage("Images/ppHop.gif", true); } catch (Exception ex) when (ex is OperationCanceledException or TaskCanceledException && _cancellationTokenSource.IsCancellationRequested) { - statusMessage.Text = Translations.Strings.StatusCanceled; + renderProgress.SetStatus(Translations.Strings.StatusCanceled); SetImage("Images/ppHop.gif", true); } catch (Exception ex) { - statusMessage.Text = Translations.Strings.StatusError; + renderProgress.SetStatus(Translations.Strings.StatusError); SetImage("Images/peepoSad.png", false); AppendLog(Translations.Strings.ErrorLog + ex.Message); if (Settings.Default.VerboseErrors) @@ -676,7 +672,7 @@ private async void SplitBtnRender_Click(object sender, RoutedEventArgs e) } } } - statusProgressBar.Value = 0; + renderProgress.ReportProgress(0); _cancellationTokenSource.Dispose(); UpdateActionButtons(false); diff --git a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs index a53de865..1b4d9a2f 100644 --- a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs +++ b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs @@ -17,6 +17,7 @@ using TwitchDownloaderCore.TwitchObjects.Gql; using TwitchDownloaderWPF.Properties; using TwitchDownloaderWPF.Services; +using TwitchDownloaderWPF.Utils; using WpfAnimatedGif; namespace TwitchDownloaderWPF @@ -257,6 +258,20 @@ private void SetEnabledCropEnd(bool isEnabled) numEndSecond.IsEnabled = isEnabled; } + private void SetPercent(int percent) + { + Dispatcher.BeginInvoke(() => + statusProgressBar.Value = percent + ); + } + + private void SetStatus(string message) + { + Dispatcher.BeginInvoke(() => + statusMessage.Text = message + ); + } + private void AppendLog(string message) { textLog.Dispatcher.BeginInvoke(() => @@ -314,22 +329,6 @@ public ChatUpdateOptions GetOptions(string outputFile) return options; } - private void OnProgressChanged(ProgressReport progress) - { - switch (progress.ReportType) - { - case ReportType.Percent: - statusProgressBar.Value = (int)progress.Data; - break; - case ReportType.NewLineStatus or ReportType.SameLineStatus: - statusMessage.Text = (string)progress.Data; - break; - case ReportType.Log: - AppendLog((string)progress.Data); - break; - } - } - public void SetImage(string imageUri, bool isGif) { var image = new BitmapImage(); @@ -510,7 +509,8 @@ private async void SplitBtnUpdate_Click(object sender, RoutedEventArgs e) { ChatUpdateOptions updateOptions = GetOptions(saveFileDialog.FileName); - ChatUpdater currentUpdate = new ChatUpdater(updateOptions); + var updateProgress = new WpfTaskProgress(SetPercent, SetStatus, AppendLog); + var currentUpdate = new ChatUpdater(updateOptions, updateProgress); try { await currentUpdate.ParseJsonAsync(CancellationToken.None); @@ -533,24 +533,21 @@ private async void SplitBtnUpdate_Click(object sender, RoutedEventArgs e) _cancellationTokenSource = new CancellationTokenSource(); UpdateActionButtons(true); - Progress updateProgress = new Progress(OnProgressChanged); - try { - await currentUpdate.UpdateAsync(updateProgress, _cancellationTokenSource.Token); - await Task.Delay(300); // we need to wait a bit incase the "writing to output file" report comes late + await currentUpdate.UpdateAsync(_cancellationTokenSource.Token); textJson.Text = ""; - statusMessage.Text = Translations.Strings.StatusDone; + updateProgress.SetStatus(Translations.Strings.StatusDone); SetImage("Images/ppHop.gif", true); } catch (Exception ex) when (ex is OperationCanceledException or TaskCanceledException && _cancellationTokenSource.IsCancellationRequested) { - statusMessage.Text = Translations.Strings.StatusCanceled; + updateProgress.SetStatus(Translations.Strings.StatusCanceled); SetImage("Images/ppHop.gif", true); } catch (Exception ex) { - statusMessage.Text = Translations.Strings.StatusError; + updateProgress.SetStatus(Translations.Strings.StatusError); SetImage("Images/peepoSad.png", false); AppendLog(Translations.Strings.ErrorLog + ex.Message); if (Settings.Default.VerboseErrors) @@ -559,7 +556,7 @@ private async void SplitBtnUpdate_Click(object sender, RoutedEventArgs e) } } btnBrowse.IsEnabled = true; - statusProgressBar.Value = 0; + updateProgress.ReportProgress(0); _cancellationTokenSource.Dispose(); UpdateActionButtons(false); diff --git a/TwitchDownloaderWPF/PageClipDownload.xaml.cs b/TwitchDownloaderWPF/PageClipDownload.xaml.cs index fa05269a..d68e600c 100644 --- a/TwitchDownloaderWPF/PageClipDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageClipDownload.xaml.cs @@ -15,6 +15,7 @@ using TwitchDownloaderCore.TwitchObjects.Gql; using TwitchDownloaderWPF.Properties; using TwitchDownloaderWPF.Services; +using TwitchDownloaderWPF.Utils; using WpfAnimatedGif; namespace TwitchDownloaderWPF @@ -118,6 +119,20 @@ private static string ValidateUrl(string text) : null; } + private void SetPercent(int percent) + { + Dispatcher.BeginInvoke(() => + statusProgressBar.Value = percent + ); + } + + private void SetStatus(string message) + { + Dispatcher.BeginInvoke(() => + statusMessage.Text = message + ); + } + private void AppendLog(string message) { textLog.Dispatcher.BeginInvoke(() => @@ -138,22 +153,6 @@ private void SetEnabled(bool enabled) CheckMetadata.IsEnabled = enabled; } - private void OnProgressChanged(ProgressReport progress) - { - switch (progress.ReportType) - { - case ReportType.Percent: - statusProgressBar.Value = (int)progress.Data; - break; - case ReportType.NewLineStatus or ReportType.SameLineStatus: - statusMessage.Text = (string)progress.Data; - break; - case ReportType.Log: - AppendLog((string)progress.Data); - break; - } - } - public void SetImage(string imageUri, bool isGif) { var image = new BitmapImage(); @@ -214,26 +213,26 @@ private async void SplitBtnDownload_Click(object sender, RoutedEventArgs e) ClipDownloadOptions downloadOptions = GetOptions(saveFileDialog.FileName); _cancellationTokenSource = new CancellationTokenSource(); + var downloadProgress = new WpfTaskProgress(SetPercent, SetStatus, AppendLog); + var currentDownload = new ClipDownloader(downloadOptions, downloadProgress); + SetImage("Images/ppOverheat.gif", true); statusMessage.Text = Translations.Strings.StatusDownloading; UpdateActionButtons(true); try { - var downloadProgress = new Progress(OnProgressChanged); - await new ClipDownloader(downloadOptions, downloadProgress) - .DownloadAsync(_cancellationTokenSource.Token); - - statusMessage.Text = Translations.Strings.StatusDone; + await currentDownload.DownloadAsync(_cancellationTokenSource.Token); + downloadProgress.SetStatus(Translations.Strings.StatusDone); SetImage("Images/ppHop.gif", true); } catch (Exception ex) when (ex is OperationCanceledException or TaskCanceledException && _cancellationTokenSource.IsCancellationRequested) { - statusMessage.Text = Translations.Strings.StatusCanceled; + downloadProgress.SetStatus(Translations.Strings.StatusCanceled); SetImage("Images/ppHop.gif", true); } catch (Exception ex) { - statusMessage.Text = Translations.Strings.StatusError; + downloadProgress.SetStatus(Translations.Strings.StatusError); SetImage("Images/peepoSad.png", false); AppendLog(Translations.Strings.ErrorLog + ex.Message); if (Settings.Default.VerboseErrors) @@ -242,7 +241,7 @@ private async void SplitBtnDownload_Click(object sender, RoutedEventArgs e) } } btnGetInfo.IsEnabled = true; - statusProgressBar.Value = 0; + downloadProgress.ReportProgress(0); _cancellationTokenSource.Dispose(); UpdateActionButtons(false); } diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs index 8443b4da..8e4406ae 100644 --- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs @@ -20,6 +20,7 @@ using TwitchDownloaderCore.TwitchObjects.Gql; using TwitchDownloaderWPF.Properties; using TwitchDownloaderWPF.Services; +using TwitchDownloaderWPF.Utils; using WpfAnimatedGif; namespace TwitchDownloaderWPF @@ -256,22 +257,6 @@ private static string GetQualityWithoutSize(string qualityWithSize) : qualityWithSize[..qualityIndex]; } - private void OnProgressChanged(ProgressReport progress) - { - switch (progress.ReportType) - { - case ReportType.Percent: - statusProgressBar.Value = (int)progress.Data; - break; - case ReportType.NewLineStatus or ReportType.SameLineStatus: - statusMessage.Text = (string)progress.Data; - break; - case ReportType.Log: - AppendLog((string)progress.Data); - break; - } - } - public void SetImage(string imageUri, bool isGif) { var image = new BitmapImage(); @@ -323,6 +308,20 @@ public bool ValidateInputs() return true; } + private void SetPercent(int percent) + { + Dispatcher.BeginInvoke(() => + statusProgressBar.Value = percent + ); + } + + private void SetStatus(string message) + { + Dispatcher.BeginInvoke(() => + statusMessage.Text = message + ); + } + private void AppendLog(string message) { textLog.Dispatcher.BeginInvoke(() => @@ -425,7 +424,7 @@ private async void SplitBtnDownloader_Click(object sender, RoutedEventArgs e) VideoDownloadOptions options = GetOptions(saveFileDialog.FileName, null); options.CacheCleanerCallback = HandleCacheCleanerCallback; - Progress downloadProgress = new Progress(OnProgressChanged); + var downloadProgress = new WpfTaskProgress(SetPercent, SetStatus, AppendLog); VideoDownloader currentDownload = new VideoDownloader(options, downloadProgress); _cancellationTokenSource = new CancellationTokenSource(); @@ -435,17 +434,17 @@ private async void SplitBtnDownloader_Click(object sender, RoutedEventArgs e) try { await currentDownload.DownloadAsync(_cancellationTokenSource.Token); - statusMessage.Text = Translations.Strings.StatusDone; + downloadProgress.SetStatus(Translations.Strings.StatusDone); SetImage("Images/ppHop.gif", true); } catch (Exception ex) when (ex is OperationCanceledException or TaskCanceledException && _cancellationTokenSource.IsCancellationRequested) { - statusMessage.Text = Translations.Strings.StatusCanceled; + downloadProgress.SetStatus(Translations.Strings.StatusCanceled); SetImage("Images/ppHop.gif", true); } catch (Exception ex) { - statusMessage.Text = Translations.Strings.StatusError; + downloadProgress.SetStatus(Translations.Strings.StatusError); SetImage("Images/peepoSad.png", false); AppendLog(Translations.Strings.ErrorLog + ex.Message); if (Settings.Default.VerboseErrors) @@ -454,7 +453,7 @@ private async void SplitBtnDownloader_Click(object sender, RoutedEventArgs e) } } btnGetInfo.IsEnabled = true; - statusProgressBar.Value = 0; + downloadProgress.ReportProgress(0); _cancellationTokenSource.Dispose(); UpdateActionButtons(false); diff --git a/TwitchDownloaderWPF/TwitchTasks/ChatDownloadTask.cs b/TwitchDownloaderWPF/TwitchTasks/ChatDownloadTask.cs index 52c610e6..45e00a97 100644 --- a/TwitchDownloaderWPF/TwitchTasks/ChatDownloadTask.cs +++ b/TwitchDownloaderWPF/TwitchTasks/ChatDownloadTask.cs @@ -1,24 +1,72 @@ using System; using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using TwitchDownloaderCore; using TwitchDownloaderCore.Options; +using TwitchDownloaderWPF.Utils; namespace TwitchDownloaderWPF.TwitchTasks { class ChatDownloadTask : ITwitchTask { public TaskData Info { get; set; } = new TaskData(); - public int Progress { get; set; } - public TwitchTaskStatus Status { get; private set; } = TwitchTaskStatus.Ready; - public ChatDownloadOptions DownloadOptions { get; set; } + + private int _progress; + public int Progress + { + get => _progress; + set + { + if (value == _progress) return; + _progress = value; + OnPropertyChanged(); + } + } + + private TwitchTaskStatus _status = TwitchTaskStatus.Ready; + public TwitchTaskStatus Status + { + get => _status; + private set + { + if (value == _status) return; + _status = value; + OnPropertyChanged(); + } + } + + public ChatDownloadOptions DownloadOptions { get; init; } public CancellationTokenSource TokenSource { get; set; } = new CancellationTokenSource(); public ITwitchTask DependantTask { get; set; } public string TaskType { get; } = Translations.Strings.ChatDownload; - public TwitchTaskException Exception { get; private set; } = new(); + + private TwitchTaskException _exception = new(); + public TwitchTaskException Exception + { + get => _exception; + private set + { + if (Equals(value, _exception)) return; + _exception = value; + OnPropertyChanged(); + } + } + public string OutputFile => DownloadOptions.Filename; - public bool CanCancel { get; private set; } = true; + + private bool _canCancel = true; + public bool CanCancel + { + get => _canCancel; + private set + { + if (value == _canCancel) return; + _canCancel = value; + OnPropertyChanged(); + } + } public event PropertyChangedEventHandler PropertyChanged; @@ -48,12 +96,10 @@ public bool CanRun() public void ChangeStatus(TwitchTaskStatus newStatus) { Status = newStatus; - OnPropertyChanged(nameof(Status)); if (CanCancel && newStatus is TwitchTaskStatus.Canceled or TwitchTaskStatus.Failed or TwitchTaskStatus.Finished or TwitchTaskStatus.Stopping) { CanCancel = false; - OnPropertyChanged(nameof(CanCancel)); } } @@ -66,21 +112,19 @@ public async Task RunAsync() return; } - ChatDownloader downloader = new ChatDownloader(DownloadOptions); - Progress progress = new Progress(); - progress.ProgressChanged += Progress_ProgressChanged; + var progress = new WpfTaskProgress(i => Progress = i); + ChatDownloader downloader = new ChatDownloader(DownloadOptions, progress); ChangeStatus(TwitchTaskStatus.Running); try { - await downloader.DownloadAsync(progress, TokenSource.Token); + await downloader.DownloadAsync(TokenSource.Token); if (TokenSource.IsCancellationRequested) { ChangeStatus(TwitchTaskStatus.Canceled); } else { - Progress = 100; - OnPropertyChanged(nameof(Progress)); + progress.ReportProgress(100); ChangeStatus(TwitchTaskStatus.Finished); } } @@ -92,7 +136,6 @@ public async Task RunAsync() { ChangeStatus(TwitchTaskStatus.Failed); Exception = new TwitchTaskException(ex); - OnPropertyChanged(nameof(Exception)); } downloader = null; TokenSource.Dispose(); @@ -100,20 +143,7 @@ public async Task RunAsync() GC.WaitForPendingFinalizers(); } - private void Progress_ProgressChanged(object sender, ProgressReport e) - { - if (e.ReportType == ReportType.Percent) - { - int percent = (int)e.Data; - if (percent > Progress) - { - Progress = percent; - OnPropertyChanged(nameof(Progress)); - } - } - } - - protected virtual void OnPropertyChanged(string propertyName) + private void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/TwitchDownloaderWPF/TwitchTasks/ChatRenderTask.cs b/TwitchDownloaderWPF/TwitchTasks/ChatRenderTask.cs index 4af99dfd..26a70ba8 100644 --- a/TwitchDownloaderWPF/TwitchTasks/ChatRenderTask.cs +++ b/TwitchDownloaderWPF/TwitchTasks/ChatRenderTask.cs @@ -1,24 +1,72 @@ using System; using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using TwitchDownloaderCore; using TwitchDownloaderCore.Options; +using TwitchDownloaderWPF.Utils; namespace TwitchDownloaderWPF.TwitchTasks { class ChatRenderTask : ITwitchTask { public TaskData Info { get; set; } = new TaskData(); - public int Progress { get; set; } - public TwitchTaskStatus Status { get; private set; } = TwitchTaskStatus.Ready; - public ChatRenderOptions DownloadOptions { get; set; } + + private int _progress; + public int Progress + { + get => _progress; + set + { + if (value == _progress) return; + _progress = value; + OnPropertyChanged(); + } + } + + private TwitchTaskStatus _status = TwitchTaskStatus.Ready; + public TwitchTaskStatus Status + { + get => _status; + private set + { + if (value == _status) return; + _status = value; + OnPropertyChanged(); + } + } + + public ChatRenderOptions DownloadOptions { get; init; } public CancellationTokenSource TokenSource { get; set; } = new CancellationTokenSource(); public ITwitchTask DependantTask { get; set; } public string TaskType { get; } = Translations.Strings.ChatRender; - public TwitchTaskException Exception { get; private set; } = new(); + + private TwitchTaskException _exception = new(); + public TwitchTaskException Exception + { + get => _exception; + private set + { + if (Equals(value, _exception)) return; + _exception = value; + OnPropertyChanged(); + } + } + public string OutputFile => DownloadOptions.OutputFile; - public bool CanCancel { get; private set; } = true; + + private bool _canCancel = true; + public bool CanCancel + { + get => _canCancel; + private set + { + if (value == _canCancel) return; + _canCancel = value; + OnPropertyChanged(); + } + } public event PropertyChangedEventHandler PropertyChanged; @@ -67,12 +115,10 @@ public bool CanRun() public void ChangeStatus(TwitchTaskStatus newStatus) { Status = newStatus; - OnPropertyChanged(nameof(Status)); if (CanCancel && newStatus is TwitchTaskStatus.Canceled or TwitchTaskStatus.Failed or TwitchTaskStatus.Finished or TwitchTaskStatus.Stopping) { CanCancel = false; - OnPropertyChanged(nameof(CanCancel)); } } @@ -85,8 +131,7 @@ public async Task RunAsync() return; } - Progress progress = new Progress(); - progress.ProgressChanged += Progress_ProgressChanged; + var progress = new WpfTaskProgress(i => Progress = i); ChatRenderer renderer = new ChatRenderer(DownloadOptions, progress); ChangeStatus(TwitchTaskStatus.Running); try @@ -99,9 +144,8 @@ public async Task RunAsync() } else { + progress.ReportProgress(100); ChangeStatus(TwitchTaskStatus.Finished); - Progress = 100; - OnPropertyChanged(nameof(Progress)); } } catch (Exception ex) when (ex is OperationCanceledException or TaskCanceledException && TokenSource.IsCancellationRequested) @@ -112,27 +156,13 @@ public async Task RunAsync() { ChangeStatus(TwitchTaskStatus.Failed); Exception = new TwitchTaskException(ex); - OnPropertyChanged(nameof(Exception)); } renderer.Dispose(); TokenSource.Dispose(); GC.Collect(2, GCCollectionMode.Default, false); } - private void Progress_ProgressChanged(object sender, ProgressReport e) - { - if (e.ReportType == ReportType.Percent) - { - int percent = (int)e.Data; - if (percent > Progress) - { - Progress = percent; - OnPropertyChanged(nameof(Progress)); - } - } - } - - protected virtual void OnPropertyChanged(string propertyName) + private void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/TwitchDownloaderWPF/TwitchTasks/ChatUpdateTask.cs b/TwitchDownloaderWPF/TwitchTasks/ChatUpdateTask.cs index 2b10c7fb..d523cbea 100644 --- a/TwitchDownloaderWPF/TwitchTasks/ChatUpdateTask.cs +++ b/TwitchDownloaderWPF/TwitchTasks/ChatUpdateTask.cs @@ -1,24 +1,72 @@ using System; using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using TwitchDownloaderCore; using TwitchDownloaderCore.Options; +using TwitchDownloaderWPF.Utils; namespace TwitchDownloaderWPF.TwitchTasks { class ChatUpdateTask : ITwitchTask { public TaskData Info { get; set; } = new TaskData(); - public int Progress { get; set; } - public TwitchTaskStatus Status { get; private set; } = TwitchTaskStatus.Ready; - public ChatUpdateOptions UpdateOptions { get; set; } + + private int _progress; + public int Progress + { + get => _progress; + set + { + if (value == _progress) return; + _progress = value; + OnPropertyChanged(); + } + } + + private TwitchTaskStatus _status = TwitchTaskStatus.Ready; + public TwitchTaskStatus Status + { + get => _status; + private set + { + if (value == _status) return; + _status = value; + OnPropertyChanged(); + } + } + + public ChatUpdateOptions UpdateOptions { get; init; } public CancellationTokenSource TokenSource { get; set; } = new CancellationTokenSource(); public ITwitchTask DependantTask { get; set; } public string TaskType { get; } = Translations.Strings.ChatUpdate; - public TwitchTaskException Exception { get; private set; } = new(); + + private TwitchTaskException _exception = new(); + public TwitchTaskException Exception + { + get => _exception; + private set + { + if (Equals(value, _exception)) return; + _exception = value; + OnPropertyChanged(); + } + } + public string OutputFile => UpdateOptions.OutputFile; - public bool CanCancel { get; private set; } = true; + + private bool _canCancel = true; + public bool CanCancel + { + get => _canCancel; + private set + { + if (value == _canCancel) return; + _canCancel = value; + OnPropertyChanged(); + } + } public event PropertyChangedEventHandler PropertyChanged; @@ -48,12 +96,10 @@ public bool CanRun() public void ChangeStatus(TwitchTaskStatus newStatus) { Status = newStatus; - OnPropertyChanged(nameof(Status)); if (CanCancel && newStatus is TwitchTaskStatus.Canceled or TwitchTaskStatus.Failed or TwitchTaskStatus.Finished or TwitchTaskStatus.Stopping) { CanCancel = false; - OnPropertyChanged(nameof(CanCancel)); } } @@ -66,22 +112,20 @@ public async Task RunAsync() return; } - ChatUpdater updater = new ChatUpdater(UpdateOptions); - Progress progress = new Progress(); - progress.ProgressChanged += Progress_ProgressChanged; + var progress = new WpfTaskProgress(i => Progress = i); + ChatUpdater updater = new ChatUpdater(UpdateOptions, progress); ChangeStatus(TwitchTaskStatus.Running); try { await updater.ParseJsonAsync(TokenSource.Token); - await updater.UpdateAsync(progress, TokenSource.Token); + await updater.UpdateAsync(TokenSource.Token); if (TokenSource.IsCancellationRequested) { ChangeStatus(TwitchTaskStatus.Canceled); } else { - Progress = 100; - OnPropertyChanged(nameof(Progress)); + progress.ReportProgress(100); ChangeStatus(TwitchTaskStatus.Finished); } } @@ -93,7 +137,6 @@ public async Task RunAsync() { ChangeStatus(TwitchTaskStatus.Failed); Exception = new TwitchTaskException(ex); - OnPropertyChanged(nameof(Exception)); } updater = null; TokenSource.Dispose(); @@ -101,20 +144,7 @@ public async Task RunAsync() GC.WaitForPendingFinalizers(); } - private void Progress_ProgressChanged(object sender, ProgressReport e) - { - if (e.ReportType == ReportType.Percent) - { - int percent = (int)e.Data; - if (percent > Progress) - { - Progress = percent; - OnPropertyChanged(nameof(Progress)); - } - } - } - - protected virtual void OnPropertyChanged(string propertyName) + private void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/TwitchDownloaderWPF/TwitchTasks/ClipDownloadTask.cs b/TwitchDownloaderWPF/TwitchTasks/ClipDownloadTask.cs index 274f4565..c8ddfbcc 100644 --- a/TwitchDownloaderWPF/TwitchTasks/ClipDownloadTask.cs +++ b/TwitchDownloaderWPF/TwitchTasks/ClipDownloadTask.cs @@ -1,24 +1,72 @@ using System; using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using TwitchDownloaderCore; using TwitchDownloaderCore.Options; +using TwitchDownloaderWPF.Utils; namespace TwitchDownloaderWPF.TwitchTasks { class ClipDownloadTask : ITwitchTask { public TaskData Info { get; set; } = new TaskData(); - public int Progress { get; set; } - public TwitchTaskStatus Status { get; private set; } = TwitchTaskStatus.Ready; - public ClipDownloadOptions DownloadOptions { get; set; } + + private int _progress; + public int Progress + { + get => _progress; + set + { + if (value == _progress) return; + _progress = value; + OnPropertyChanged(); + } + } + + private TwitchTaskStatus _status = TwitchTaskStatus.Ready; + public TwitchTaskStatus Status + { + get => _status; + private set + { + if (value == _status) return; + _status = value; + OnPropertyChanged(); + } + } + + public ClipDownloadOptions DownloadOptions { get; init; } public CancellationTokenSource TokenSource { get; set; } = new CancellationTokenSource(); public ITwitchTask DependantTask { get; set; } public string TaskType { get; } = Translations.Strings.ClipDownload; - public TwitchTaskException Exception { get; private set; } = new(); + + private TwitchTaskException _exception = new(); + public TwitchTaskException Exception + { + get => _exception; + private set + { + if (Equals(value, _exception)) return; + _exception = value; + OnPropertyChanged(); + } + } + public string OutputFile => DownloadOptions.Filename; - public bool CanCancel { get; private set; } = true; + + private bool _canCancel = true; + public bool CanCancel + { + get => _canCancel; + private set + { + if (value == _canCancel) return; + _canCancel = value; + OnPropertyChanged(); + } + } public event PropertyChangedEventHandler PropertyChanged; @@ -48,12 +96,10 @@ public bool CanRun() public void ChangeStatus(TwitchTaskStatus newStatus) { Status = newStatus; - OnPropertyChanged(nameof(Status)); if (CanCancel && newStatus is TwitchTaskStatus.Canceled or TwitchTaskStatus.Failed or TwitchTaskStatus.Finished or TwitchTaskStatus.Stopping) { CanCancel = false; - OnPropertyChanged(nameof(CanCancel)); } } @@ -66,8 +112,7 @@ public async Task RunAsync() return; } - Progress progress = new Progress(); - progress.ProgressChanged += Progress_ProgressChanged; + var progress = new WpfTaskProgress(i => Progress = i); ClipDownloader downloader = new ClipDownloader(DownloadOptions, progress); ChangeStatus(TwitchTaskStatus.Running); try @@ -79,8 +124,7 @@ public async Task RunAsync() } else { - Progress = 100; - OnPropertyChanged(nameof(Progress)); + progress.ReportProgress(100); ChangeStatus(TwitchTaskStatus.Finished); } } @@ -92,7 +136,6 @@ public async Task RunAsync() { ChangeStatus(TwitchTaskStatus.Failed); Exception = new TwitchTaskException(ex); - OnPropertyChanged(nameof(Exception)); } downloader = null; TokenSource.Dispose(); @@ -100,20 +143,7 @@ public async Task RunAsync() GC.WaitForPendingFinalizers(); } - private void Progress_ProgressChanged(object sender, ProgressReport e) - { - if (e.ReportType == ReportType.Percent) - { - int percent = (int)e.Data; - if (percent > Progress) - { - Progress = percent; - OnPropertyChanged(nameof(Progress)); - } - } - } - - protected virtual void OnPropertyChanged(string propertyName) + private void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/TwitchDownloaderWPF/TwitchTasks/VodDownloadTask.cs b/TwitchDownloaderWPF/TwitchTasks/VodDownloadTask.cs index 95f64074..3f0447e5 100644 --- a/TwitchDownloaderWPF/TwitchTasks/VodDownloadTask.cs +++ b/TwitchDownloaderWPF/TwitchTasks/VodDownloadTask.cs @@ -1,24 +1,72 @@ using System; using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using TwitchDownloaderCore; using TwitchDownloaderCore.Options; +using TwitchDownloaderWPF.Utils; namespace TwitchDownloaderWPF.TwitchTasks { class VodDownloadTask : ITwitchTask { public TaskData Info { get; set; } = new TaskData(); - public int Progress { get; set; } - public TwitchTaskStatus Status { get; private set; } = TwitchTaskStatus.Ready; - public VideoDownloadOptions DownloadOptions { get; set; } + + private int _progress; + public int Progress + { + get => _progress; + set + { + if (value == _progress) return; + _progress = value; + OnPropertyChanged(); + } + } + + private TwitchTaskStatus _status = TwitchTaskStatus.Ready; + public TwitchTaskStatus Status + { + get => _status; + private set + { + if (value == _status) return; + _status = value; + OnPropertyChanged(); + } + } + + public VideoDownloadOptions DownloadOptions { get; init; } public CancellationTokenSource TokenSource { get; set; } = new CancellationTokenSource(); public ITwitchTask DependantTask { get; set; } public string TaskType { get; } = Translations.Strings.VodDownload; - public TwitchTaskException Exception { get; private set; } = new(); + + private TwitchTaskException _exception = new(); + public TwitchTaskException Exception + { + get => _exception; + private set + { + if (Equals(value, _exception)) return; + _exception = value; + OnPropertyChanged(); + } + } + public string OutputFile => DownloadOptions.Filename; - public bool CanCancel { get; private set; } = true; + + private bool _canCancel = true; + public bool CanCancel + { + get => _canCancel; + private set + { + if (value == _canCancel) return; + _canCancel = value; + OnPropertyChanged(); + } + } public event PropertyChangedEventHandler PropertyChanged; @@ -48,12 +96,10 @@ public bool CanRun() public void ChangeStatus(TwitchTaskStatus newStatus) { Status = newStatus; - OnPropertyChanged(nameof(Status)); if (CanCancel && newStatus is TwitchTaskStatus.Canceled or TwitchTaskStatus.Failed or TwitchTaskStatus.Finished or TwitchTaskStatus.Stopping) { CanCancel = false; - OnPropertyChanged(nameof(CanCancel)); } } @@ -66,8 +112,7 @@ public async Task RunAsync() return; } - Progress progress = new Progress(); - progress.ProgressChanged += Progress_ProgressChanged; + var progress = new WpfTaskProgress(i => Progress = i); VideoDownloader downloader = new VideoDownloader(DownloadOptions, progress); ChangeStatus(TwitchTaskStatus.Running); try @@ -79,8 +124,7 @@ public async Task RunAsync() } else { - Progress = 100; - OnPropertyChanged(nameof(Progress)); + progress.ReportProgress(100); ChangeStatus(TwitchTaskStatus.Finished); } } @@ -92,7 +136,6 @@ public async Task RunAsync() { ChangeStatus(TwitchTaskStatus.Failed); Exception = new TwitchTaskException(ex); - OnPropertyChanged(nameof(Exception)); } downloader = null; TokenSource.Dispose(); @@ -100,20 +143,7 @@ public async Task RunAsync() GC.WaitForPendingFinalizers(); } - private void Progress_ProgressChanged(object sender, ProgressReport e) - { - if (e.ReportType == ReportType.Percent) - { - int percent = (int)e.Data; - if (percent > Progress) - { - Progress = percent; - OnPropertyChanged(nameof(Progress)); - } - } - } - - protected virtual void OnPropertyChanged(string propertyName) + private void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/TwitchDownloaderWPF/Utils/WpfTaskProgress.cs b/TwitchDownloaderWPF/Utils/WpfTaskProgress.cs new file mode 100644 index 00000000..294f3edf --- /dev/null +++ b/TwitchDownloaderWPF/Utils/WpfTaskProgress.cs @@ -0,0 +1,143 @@ +using System; +using TwitchDownloaderCore.Interfaces; + +namespace TwitchDownloaderWPF.Utils +{ + // TODO: Implement log levels + internal class WpfTaskProgress : ITaskProgress + { + private string _status; + private bool _statusIsTemplate; + + private int _lastPercent = -1; + private TimeSpan _lastTime1 = new(-1); + private TimeSpan _lastTime2 = new(-1); + + private readonly Action _handlePercent; + private readonly Action _handleStatus; + private readonly Action _handleLog; + private readonly Action _handleFfmpegLog; + + public WpfTaskProgress(Action handlePercent) + { + _handlePercent = handlePercent; + _handleStatus = null; + _handleLog = null; + _handleFfmpegLog = null; + } + + public WpfTaskProgress(Action handlePercent, Action handleStatus, Action handleLog, Action handleFfmpegLog = null) + { + _handlePercent = handlePercent; + _handleStatus = handleStatus; + _handleLog = handleLog; + _handleFfmpegLog = handleFfmpegLog; + } + + public void SetStatus(string status) + { + lock (this) + { + _status = status; + _statusIsTemplate = false; + + _handleStatus?.Invoke(status); + } + } + + public void SetTemplateStatus(string status, int initialPercent) + { + lock (this) + { + _status = status; + _statusIsTemplate = true; + + _lastPercent = -1; // Ensure that the progress report runs + ReportProgress(initialPercent); + } + } + + public void SetTemplateStatus(string status, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2) + { + lock (this) + { + _status = status; + _statusIsTemplate = true; + + _lastPercent = -1; // Ensure that the progress report runs + ReportProgress(initialPercent, initialTime1, initialTime2); + } + } + + public void ReportProgress(int percent) + { + lock (this) + { + if (_lastPercent == percent) + { + return; + } + + _handlePercent(percent); + _lastPercent = percent; + + if (!_statusIsTemplate) + { + return; + } + + var status = string.Format(_status, percent); + _handleStatus?.Invoke(status); + } + } + + public void ReportProgress(int percent, TimeSpan time1, TimeSpan time2) + { + lock (this) + { + if (_lastPercent == percent && _lastTime1 == time1 && _lastTime2 == time2) + { + return; + } + + _handlePercent(percent); + _lastPercent = percent; + _lastTime1 = time1; + _lastTime2 = time2; + + if (!_statusIsTemplate) + { + return; + } + + var status = string.Format(_status, percent, time1, time2); + _handleStatus?.Invoke(status); + } + } + + public void LogVerbose(string logMessage) + { + //_handleLog?.Invoke(logMessage); + } + + public void LogInfo(string logMessage) + { + _handleLog?.Invoke(logMessage); + } + + public void LogWarning(string logMessage) + { + _handleLog?.Invoke(logMessage); + } + + public void LogError(string logMessage) + { + _handleLog?.Invoke(Translations.Strings.ErrorLog + logMessage); + } + + public void LogFfmpeg(string logMessage) + { + _handleFfmpegLog?.Invoke(logMessage); + } + } +} \ No newline at end of file