From 7a26013f99d52b359659be2950f396c025b57697 Mon Sep 17 00:00:00 2001
From: Christopher Stelma
Date: Tue, 8 Aug 2023 15:00:21 -0700
Subject: [PATCH 01/50] Triple click template problem (#786)
* Revert "Remove triple click from URL mass downloader text box (#783)"
This reverts commit 559e5d9cce143cf6cfc35784d5386c2a4a0ce967.
* remove template override?
* Revert "remove template override?"
This reverts commit 499c88d3161270bddc111d298402aa8bf780635b.
* kinda
* better?
* some layout stuff
* cleanup; w/s + center button
* cleanup
* format
* review comments: remove redundant
* visual order
* review feedback: remove template setter on other windows/pages
* review feedback: space between elements
---
TwitchDownloaderWPF/PageChatDownload.xaml | 1 -
TwitchDownloaderWPF/PageChatRender.xaml | 1 -
TwitchDownloaderWPF/PageChatUpdate.xaml | 1 -
TwitchDownloaderWPF/PageClipDownload.xaml | 1 -
TwitchDownloaderWPF/PageVodDownload.xaml | 1 -
TwitchDownloaderWPF/WindowMassDownload.xaml | 1 -
TwitchDownloaderWPF/WindowQueueOptions.xaml | 1 -
TwitchDownloaderWPF/WindowRangeSelect.xaml | 1 -
TwitchDownloaderWPF/WindowSettings.xaml | 1 -
TwitchDownloaderWPF/WindowUrlList.xaml | 19 +++++++++++++++----
10 files changed, 15 insertions(+), 13 deletions(-)
diff --git a/TwitchDownloaderWPF/PageChatDownload.xaml b/TwitchDownloaderWPF/PageChatDownload.xaml
index f23bdf5a..c95fbf85 100644
--- a/TwitchDownloaderWPF/PageChatDownload.xaml
+++ b/TwitchDownloaderWPF/PageChatDownload.xaml
@@ -20,7 +20,6 @@
diff --git a/TwitchDownloaderWPF/PageChatRender.xaml b/TwitchDownloaderWPF/PageChatRender.xaml
index 3f7a2d25..c7560c89 100644
--- a/TwitchDownloaderWPF/PageChatRender.xaml
+++ b/TwitchDownloaderWPF/PageChatRender.xaml
@@ -20,7 +20,6 @@
diff --git a/TwitchDownloaderWPF/PageChatUpdate.xaml b/TwitchDownloaderWPF/PageChatUpdate.xaml
index adda5771..1eb270f3 100644
--- a/TwitchDownloaderWPF/PageChatUpdate.xaml
+++ b/TwitchDownloaderWPF/PageChatUpdate.xaml
@@ -20,7 +20,6 @@
diff --git a/TwitchDownloaderWPF/PageClipDownload.xaml b/TwitchDownloaderWPF/PageClipDownload.xaml
index 3d2c6903..c875b23e 100644
--- a/TwitchDownloaderWPF/PageClipDownload.xaml
+++ b/TwitchDownloaderWPF/PageClipDownload.xaml
@@ -19,7 +19,6 @@
diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml b/TwitchDownloaderWPF/PageVodDownload.xaml
index f554e9eb..ed2be6a8 100644
--- a/TwitchDownloaderWPF/PageVodDownload.xaml
+++ b/TwitchDownloaderWPF/PageVodDownload.xaml
@@ -21,7 +21,6 @@
diff --git a/TwitchDownloaderWPF/WindowMassDownload.xaml b/TwitchDownloaderWPF/WindowMassDownload.xaml
index 1beebb7c..ac9b3ad5 100644
--- a/TwitchDownloaderWPF/WindowMassDownload.xaml
+++ b/TwitchDownloaderWPF/WindowMassDownload.xaml
@@ -15,7 +15,6 @@
diff --git a/TwitchDownloaderWPF/WindowQueueOptions.xaml b/TwitchDownloaderWPF/WindowQueueOptions.xaml
index 839afb5a..2117bbe7 100644
--- a/TwitchDownloaderWPF/WindowQueueOptions.xaml
+++ b/TwitchDownloaderWPF/WindowQueueOptions.xaml
@@ -14,7 +14,6 @@
diff --git a/TwitchDownloaderWPF/WindowRangeSelect.xaml b/TwitchDownloaderWPF/WindowRangeSelect.xaml
index c9f45d2e..26e05f24 100644
--- a/TwitchDownloaderWPF/WindowRangeSelect.xaml
+++ b/TwitchDownloaderWPF/WindowRangeSelect.xaml
@@ -14,7 +14,6 @@
diff --git a/TwitchDownloaderWPF/WindowSettings.xaml b/TwitchDownloaderWPF/WindowSettings.xaml
index fb4fc65e..a36385c2 100644
--- a/TwitchDownloaderWPF/WindowSettings.xaml
+++ b/TwitchDownloaderWPF/WindowSettings.xaml
@@ -16,7 +16,6 @@
diff --git a/TwitchDownloaderWPF/WindowUrlList.xaml b/TwitchDownloaderWPF/WindowUrlList.xaml
index fdb1d966..2c06b2c0 100644
--- a/TwitchDownloaderWPF/WindowUrlList.xaml
+++ b/TwitchDownloaderWPF/WindowUrlList.xaml
@@ -4,16 +4,27 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TwitchDownloaderWPF"
+ xmlns:behave="clr-namespace:TwitchDownloaderWPF.Behaviors"
xmlns:lex="http://wpflocalizeextension.codeplex.com"
lex:LocalizeDictionary.DesignCulture=""
lex:ResxLocalizationProvider.DefaultAssembly="TwitchDownloaderWPF"
lex:ResxLocalizationProvider.DefaultDictionary="Strings"
mc:Ignorable="d"
Title="Mass Download URL List" MinHeight="590" Height="600" MinWidth="485" Width="500" Loaded="Window_Loaded">
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
From 9cc0e9c1dbedebd01e881260c5f0968980fd538f Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Thu, 10 Aug 2023 17:29:00 -0400
Subject: [PATCH 02/50] Account for alternate video highlight url format (#788)
* Account for alternate video highlight url format, increase maintainability of video/clip id regex matching
* Reuse code
---
TwitchDownloaderCLI/Modes/DownloadChat.cs | 7 +--
TwitchDownloaderCLI/Modes/DownloadClip.cs | 7 +--
TwitchDownloaderCLI/Modes/DownloadVideo.cs | 7 +--
TwitchDownloaderCore/Tools/TwitchRegex.cs | 63 ++++++++++++++++++++
TwitchDownloaderWPF/PageChatDownload.xaml.cs | 8 +--
TwitchDownloaderWPF/PageClipDownload.xaml.cs | 7 +--
TwitchDownloaderWPF/PageVodDownload.xaml.cs | 7 +--
7 files changed, 82 insertions(+), 24 deletions(-)
create mode 100644 TwitchDownloaderCore/Tools/TwitchRegex.cs
diff --git a/TwitchDownloaderCLI/Modes/DownloadChat.cs b/TwitchDownloaderCLI/Modes/DownloadChat.cs
index e645d323..cf6bb9a9 100644
--- a/TwitchDownloaderCLI/Modes/DownloadChat.cs
+++ b/TwitchDownloaderCLI/Modes/DownloadChat.cs
@@ -1,12 +1,12 @@
using System;
using System.IO;
-using System.Text.RegularExpressions;
using System.Threading;
using TwitchDownloaderCLI.Modes.Arguments;
using TwitchDownloaderCLI.Tools;
using TwitchDownloaderCore;
using TwitchDownloaderCore.Chat;
using TwitchDownloaderCore.Options;
+using TwitchDownloaderCore.Tools;
namespace TwitchDownloaderCLI.Modes
{
@@ -30,9 +30,8 @@ private static ChatDownloadOptions GetDownloadOptions(ChatDownloadArgs inputOpti
Environment.Exit(1);
}
- var vodClipIdRegex = new Regex(@"(?<=^|(?:clips\.)?twitch\.tv\/(?:videos|\S+\/clip)?\/?)[\w-]+?(?=$|\?)");
- var vodClipIdMatch = vodClipIdRegex.Match(inputOptions.Id);
- if (!vodClipIdMatch.Success)
+ var vodClipIdMatch = TwitchRegex.MatchVideoOrClipId(inputOptions.Id);
+ if (vodClipIdMatch is not { Success: true })
{
Console.WriteLine("[ERROR] - Unable to parse Vod/Clip ID/URL.");
Environment.Exit(1);
diff --git a/TwitchDownloaderCLI/Modes/DownloadClip.cs b/TwitchDownloaderCLI/Modes/DownloadClip.cs
index 7f12e654..321b1b52 100644
--- a/TwitchDownloaderCLI/Modes/DownloadClip.cs
+++ b/TwitchDownloaderCLI/Modes/DownloadClip.cs
@@ -1,11 +1,11 @@
using System;
using System.IO;
-using System.Text.RegularExpressions;
using System.Threading;
using TwitchDownloaderCLI.Modes.Arguments;
using TwitchDownloaderCLI.Tools;
using TwitchDownloaderCore;
using TwitchDownloaderCore.Options;
+using TwitchDownloaderCore.Tools;
namespace TwitchDownloaderCLI.Modes
{
@@ -35,9 +35,8 @@ private static ClipDownloadOptions GetDownloadOptions(ClipDownloadArgs inputOpti
Environment.Exit(1);
}
- var clipIdRegex = new Regex(@"(?<=^|(?:clips\.)?twitch\.tv\/(?:\S+\/clip)?\/?)[\w-]+?(?=$|\?)");
- var clipIdMatch = clipIdRegex.Match(inputOptions.Id);
- if (!clipIdMatch.Success)
+ var clipIdMatch = TwitchRegex.MatchClipId(inputOptions.Id);
+ if (clipIdMatch is not { Success: true })
{
Console.WriteLine("[ERROR] - Unable to parse Clip ID/URL.");
Environment.Exit(1);
diff --git a/TwitchDownloaderCLI/Modes/DownloadVideo.cs b/TwitchDownloaderCLI/Modes/DownloadVideo.cs
index 07d85a7b..0f96a4b0 100644
--- a/TwitchDownloaderCLI/Modes/DownloadVideo.cs
+++ b/TwitchDownloaderCLI/Modes/DownloadVideo.cs
@@ -1,11 +1,11 @@
using System;
using System.IO;
-using System.Text.RegularExpressions;
using System.Threading;
using TwitchDownloaderCLI.Modes.Arguments;
using TwitchDownloaderCLI.Tools;
using TwitchDownloaderCore;
using TwitchDownloaderCore.Options;
+using TwitchDownloaderCore.Tools;
namespace TwitchDownloaderCLI.Modes
{
@@ -31,9 +31,8 @@ private static VideoDownloadOptions GetDownloadOptions(VideoDownloadArgs inputOp
Environment.Exit(1);
}
- var vodIdRegex = new Regex(@"(?<=^|twitch\.tv\/videos\/)\d+(?=$|\?)");
- var vodIdMatch = vodIdRegex.Match(inputOptions.Id);
- if (!vodIdMatch.Success)
+ var vodIdMatch = TwitchRegex.MatchVideoId(inputOptions.Id);
+ if (vodIdMatch is not { Success: true})
{
Console.WriteLine("[ERROR] - Unable to parse Vod ID/URL.");
Environment.Exit(1);
diff --git a/TwitchDownloaderCore/Tools/TwitchRegex.cs b/TwitchDownloaderCore/Tools/TwitchRegex.cs
new file mode 100644
index 00000000..8905a2e2
--- /dev/null
+++ b/TwitchDownloaderCore/Tools/TwitchRegex.cs
@@ -0,0 +1,63 @@
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace TwitchDownloaderCore.Tools
+{
+ public static class TwitchRegex
+ {
+ // TODO: Use source generators when .NET7
+ private static readonly Regex VideoId = new(@"(?<=^|twitch\.tv\/videos\/)\d+(?=$|\?|\s)", RegexOptions.Compiled);
+ private static readonly Regex HighlightId = new(@"(?<=^|twitch\.tv\/\w+\/video\/)\d+(?=$|\?|\s)", RegexOptions.Compiled);
+ private static readonly Regex ClipId = new(@"(?<=^|(?:clips\.)?twitch\.tv\/(?:\w+\/clip\/)?)[\w-]+?(?=$|\?|\s)", RegexOptions.Compiled);
+
+ public static readonly Regex UrlTimeCode = new(@"(?<=(?:\?|&)t=)\d+h\d+m\d+s(?=$|\?|\s)", RegexOptions.Compiled);
+
+ /// A of the video's id or .
+ public static Match MatchVideoId(string text)
+ {
+ var videoIdMatch = VideoId.Match(text);
+ if (videoIdMatch.Success)
+ {
+ return videoIdMatch;
+ }
+
+ var highlightIdMatch = HighlightId.Match(text);
+ if (highlightIdMatch.Success)
+ {
+ return highlightIdMatch;
+ }
+
+ return null;
+ }
+
+ /// A of the clip's id or .
+ public static Match MatchClipId(string text)
+ {
+ var clipIdMatch = ClipId.Match(text);
+ if (clipIdMatch.Success && !clipIdMatch.Value.All(char.IsDigit))
+ {
+ return clipIdMatch;
+ }
+
+ return null;
+ }
+
+ /// A of the video/clip's id or .
+ public static Match MatchVideoOrClipId(string text)
+ {
+ var videoIdMatch = MatchVideoId(text);
+ if (videoIdMatch is { Success: true })
+ {
+ return videoIdMatch;
+ }
+
+ var clipIdMatch = MatchClipId(text);
+ if (clipIdMatch is { Success: true })
+ {
+ return clipIdMatch;
+ }
+
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/PageChatDownload.xaml.cs b/TwitchDownloaderWPF/PageChatDownload.xaml.cs
index cb63fea0..9f59d6ea 100644
--- a/TwitchDownloaderWPF/PageChatDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageChatDownload.xaml.cs
@@ -3,7 +3,6 @@
using System.Diagnostics;
using System.Globalization;
using System.Linq;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@@ -14,6 +13,7 @@
using TwitchDownloaderCore.Chat;
using TwitchDownloaderCore.Extensions;
using TwitchDownloaderCore.Options;
+using TwitchDownloaderCore.Tools;
using TwitchDownloaderCore.TwitchObjects.Gql;
using TwitchDownloaderWPF.Properties;
using TwitchDownloaderWPF.Services;
@@ -140,7 +140,7 @@ private async Task GetVideoInfo()
streamerId = int.Parse(videoInfo.data.video.owner.id);
viewCount = videoInfo.data.video.viewCount;
game = videoInfo.data.video.game?.displayName ?? "Unknown";
- var urlTimeCodeMatch = Regex.Match(textUrl.Text, @"(?<=\?t=)\d+h\d+m\d+s");
+ var urlTimeCodeMatch = TwitchRegex.UrlTimeCode.Match(textUrl.Text);
if (urlTimeCodeMatch.Success)
{
var time = TimeSpanExtensions.ParseTimeCode(urlTimeCodeMatch.ValueSpan);
@@ -224,8 +224,8 @@ private void UpdateActionButtons(bool isDownloading)
public static string ValidateUrl(string text)
{
- var vodClipIdMatch = Regex.Match(text, @"(?<=^|(?:clips\.)?twitch\.tv\/(?:videos|\S+\/clip)?\/?)[\w-]+?(?=$|\?)");
- return vodClipIdMatch.Success
+ var vodClipIdMatch = TwitchRegex.MatchVideoOrClipId(text);
+ return vodClipIdMatch is { Success: true }
? vodClipIdMatch.Value
: null;
}
diff --git a/TwitchDownloaderWPF/PageClipDownload.xaml.cs b/TwitchDownloaderWPF/PageClipDownload.xaml.cs
index 891070d0..634147a1 100644
--- a/TwitchDownloaderWPF/PageClipDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageClipDownload.xaml.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@@ -12,6 +11,7 @@
using System.Windows.Media.Imaging;
using TwitchDownloaderCore;
using TwitchDownloaderCore.Options;
+using TwitchDownloaderCore.Tools;
using TwitchDownloaderCore.TwitchObjects.Gql;
using TwitchDownloaderWPF.Properties;
using TwitchDownloaderWPF.Services;
@@ -118,9 +118,8 @@ private void UpdateActionButtons(bool isDownloading)
private static string ValidateUrl(string text)
{
- var clipIdRegex = new Regex(@"(?<=^|(?:clips\.)?twitch\.tv\/(?:\S+\/clip)?\/?)[\w-]+?(?=$|\?)");
- var clipIdMatch = clipIdRegex.Match(text);
- return clipIdMatch.Success
+ var clipIdMatch = TwitchRegex.MatchClipId(text);
+ return clipIdMatch is { Success: true }
? clipIdMatch.Value
: null;
}
diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
index 49ee1778..917c2016 100644
--- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
@@ -6,7 +6,6 @@
using System.IO;
using System.Linq;
using System.Net;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@@ -150,7 +149,7 @@ private async Task GetVideoInfo()
var videoCreatedAt = taskVideoInfo.Result.data.video.createdAt;
textCreatedAt.Text = Settings.Default.UTCVideoTime ? videoCreatedAt.ToString(CultureInfo.CurrentCulture) : videoCreatedAt.ToLocalTime().ToString(CultureInfo.CurrentCulture);
currentVideoTime = Settings.Default.UTCVideoTime ? videoCreatedAt : videoCreatedAt.ToLocalTime();
- var urlTimeCodeMatch = Regex.Match(textUrl.Text, @"(?<=\?t=)\d+h\d+m\d+s");
+ var urlTimeCodeMatch = TwitchRegex.UrlTimeCode.Match(textUrl.Text);
if (urlTimeCodeMatch.Success)
{
var time = TimeSpanExtensions.ParseTimeCode(urlTimeCodeMatch.ValueSpan);
@@ -296,8 +295,8 @@ public void SetImage(string imageUri, bool isGif)
private static int ValidateUrl(string text)
{
- var vodIdMatch = Regex.Match(text, @"(?<=^|twitch\.tv\/videos\/)\d+(?=$|\?)");
- if (vodIdMatch.Success && int.TryParse(vodIdMatch.ValueSpan, out var vodId))
+ var vodIdMatch = TwitchRegex.MatchVideoId(text);
+ if (vodIdMatch is {Success: true} && int.TryParse(vodIdMatch.ValueSpan, out var vodId))
{
return vodId;
}
From 36773a8e383088d5f3c2fd4cd4917f76207c26fc Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Thu, 17 Aug 2023 00:40:37 -0400
Subject: [PATCH 03/50] Remove working directory override in CLI (#793)
Fixes an issue with relative paths in MacOS
---
TwitchDownloaderCLI/Program.cs | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/TwitchDownloaderCLI/Program.cs b/TwitchDownloaderCLI/Program.cs
index 20a96813..93b810d7 100644
--- a/TwitchDownloaderCLI/Program.cs
+++ b/TwitchDownloaderCLI/Program.cs
@@ -12,13 +12,10 @@
namespace TwitchDownloaderCLI
{
- class Program
+ internal static class Program
{
private static void Main(string[] args)
{
- // Set the working dir to the app dir in case we inherited a different working dir
- Directory.SetCurrentDirectory(AppContext.BaseDirectory);
-
var preParsedArgs = PreParseArgs.Parse(args, Path.GetFileName(Environment.ProcessPath));
var parser = new Parser(config =>
From dc64e8ad49335c5f812a283526c19d786478e0cc Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Tue, 22 Aug 2023 23:15:17 -0400
Subject: [PATCH 04/50] Fix WPF crash due to system theme watcher related
COMExceptions (#796)
* Catch potential ExternalExceptions when starting WindowsThemeService
* Rename locale key TaskError to TaskErrorButton
* Decorate some windows only functions with SupportedOSPlatformAttribute
* Fix comment
---
TwitchDownloaderWPF/PageQueue.xaml | 2 +-
TwitchDownloaderWPF/PageQueue.xaml.cs | 2 +-
.../Services/NativeFunctions.cs | 2 ++
TwitchDownloaderWPF/Services/ThemeService.cs | 6 ++++-
.../Services/WindowsThemeService.cs | 17 +++++++++++---
.../Translations/Strings.Designer.cs | 22 +++++++++++++++++--
.../Translations/Strings.es.resx | 8 ++++++-
.../Translations/Strings.fr.resx | 8 ++++++-
.../Translations/Strings.pl.resx | 8 ++++++-
TwitchDownloaderWPF/Translations/Strings.resx | 8 ++++++-
.../Translations/Strings.ru.resx | 8 ++++++-
.../Translations/Strings.tr.resx | 8 ++++++-
.../Translations/Strings.zh.resx | 8 ++++++-
13 files changed, 92 insertions(+), 15 deletions(-)
diff --git a/TwitchDownloaderWPF/PageQueue.xaml b/TwitchDownloaderWPF/PageQueue.xaml
index b83b611c..3641c17e 100644
--- a/TwitchDownloaderWPF/PageQueue.xaml
+++ b/TwitchDownloaderWPF/PageQueue.xaml
@@ -50,7 +50,7 @@
-
+
diff --git a/TwitchDownloaderWPF/PageQueue.xaml.cs b/TwitchDownloaderWPF/PageQueue.xaml.cs
index cad1c259..f59456db 100644
--- a/TwitchDownloaderWPF/PageQueue.xaml.cs
+++ b/TwitchDownloaderWPF/PageQueue.xaml.cs
@@ -222,7 +222,7 @@ private void btnTaskError_Click(object sender, RoutedEventArgs e)
errorMessage = taskException.Exception.ToString();
}
- MessageBox.Show(errorMessage, Translations.Strings.TaskError, MessageBoxButton.OK, MessageBoxImage.Error);
+ MessageBox.Show(errorMessage, Translations.Strings.MessageBoxTitleError, MessageBoxButton.OK, MessageBoxImage.Error);
}
private void btnRemoveTask_Click(object sender, RoutedEventArgs e)
diff --git a/TwitchDownloaderWPF/Services/NativeFunctions.cs b/TwitchDownloaderWPF/Services/NativeFunctions.cs
index cc698f8d..4a33f7dc 100644
--- a/TwitchDownloaderWPF/Services/NativeFunctions.cs
+++ b/TwitchDownloaderWPF/Services/NativeFunctions.cs
@@ -1,8 +1,10 @@
using System;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
namespace TwitchDownloaderWPF.Services
{
+ [SupportedOSPlatform("windows")]
public static class NativeFunctions
{
[DllImport("dwmapi.dll", EntryPoint = "DwmSetWindowAttribute", PreserveSig = true)]
diff --git a/TwitchDownloaderWPF/Services/ThemeService.cs b/TwitchDownloaderWPF/Services/ThemeService.cs
index 9aa8d28a..8c633ca2 100644
--- a/TwitchDownloaderWPF/Services/ThemeService.cs
+++ b/TwitchDownloaderWPF/Services/ThemeService.cs
@@ -2,6 +2,7 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Windows;
using System.Windows.Media;
using System.Xml.Serialization;
@@ -26,6 +27,7 @@ public ThemeService(App app, WindowsThemeService windowsThemeService)
{
Directory.CreateDirectory("Themes");
}
+
if (!DefaultThemeService.WriteIncludedThemes())
{
MessageBox.Show(Translations.Strings.ThemesFailedToWrite, Translations.Strings.ThemesFailedToWrite, MessageBoxButton.OK, MessageBoxImage.Information);
@@ -67,6 +69,7 @@ public void ChangeAppTheme()
{
newTheme = WindowsThemeService.GetWindowsTheme();
}
+
ChangeThemePath(newTheme);
var newSkin = _darkHandyControl ? SkinType.Dark : SkinType.Default;
@@ -78,6 +81,7 @@ public void ChangeAppTheme()
}
}
+ [SupportedOSPlatform("windows")]
public void SetTitleBarTheme(WindowCollection windows)
{
// If windows 10 build is before 1903, it doesn't support dark title bars
@@ -146,4 +150,4 @@ private void SetHandyControlTheme(SkinType newSkin)
_wpfApplication.Resources.MergedDictionaries[1].Source = new Uri($"pack://application:,,,/HandyControl;component/Themes/Theme.xaml", UriKind.Absolute);
}
}
-}
+}
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Services/WindowsThemeService.cs b/TwitchDownloaderWPF/Services/WindowsThemeService.cs
index 2d7cb366..04bea753 100644
--- a/TwitchDownloaderWPF/Services/WindowsThemeService.cs
+++ b/TwitchDownloaderWPF/Services/WindowsThemeService.cs
@@ -1,11 +1,14 @@
using Microsoft.Win32;
using System;
using System.Management;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Security.Principal;
using System.Windows;
namespace TwitchDownloaderWPF.Services
{
+ [SupportedOSPlatform("windows")]
public class WindowsThemeService : ManagementEventWatcher
{
public event EventHandler ThemeChanged;
@@ -17,8 +20,9 @@ public class WindowsThemeService : ManagementEventWatcher
public WindowsThemeService()
{
- // If windows version is before windows 10 or the windows 10 build is before 1809, it doesn't have the app theme registry key
- if (Environment.OSVersion.Version.Major < 10 || Environment.OSVersion.Version.Build < 17763)
+ // If the OS is older than Windows 10 1809 then it doesn't have the app theme registry key
+ const int WINDOWS_1809_BUILD_NUMBER = 17763;
+ if (Environment.OSVersion.Version.Major < 10 || Environment.OSVersion.Version.Build < WINDOWS_1809_BUILD_NUMBER)
{
return;
}
@@ -33,7 +37,14 @@ public WindowsThemeService()
Query = new EventQuery(windowsQuery);
EventArrived += WindowsThemeService_EventArrived;
- Start();
+ try
+ {
+ Start();
+ }
+ catch (ExternalException e)
+ {
+ MessageBox.Show(string.Format(Translations.Strings.UnableToStartWindowsThemeWatcher, $"0x{e.ErrorCode:x8}"), Translations.Strings.MessageBoxTitleError, MessageBoxButton.OK, MessageBoxImage.Error);
+ }
}
private void WindowsThemeService_EventArrived(object sender, EventArrivedEventArgs e)
diff --git a/TwitchDownloaderWPF/Translations/Strings.Designer.cs b/TwitchDownloaderWPF/Translations/Strings.Designer.cs
index 48fd96f9..36a31fcb 100644
--- a/TwitchDownloaderWPF/Translations/Strings.Designer.cs
+++ b/TwitchDownloaderWPF/Translations/Strings.Designer.cs
@@ -1004,6 +1004,15 @@ public static string MaximumThreadBandwidthTooltip {
}
}
+ ///
+ /// Looks up a localized string similar to Error.
+ ///
+ public static string MessageBoxTitleError {
+ get {
+ return ResourceManager.GetString("MessageBoxTitleError", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Dispersion .
///
@@ -1457,9 +1466,9 @@ public static string TaskCouldNotBeRemoved {
///
/// Looks up a localized string similar to Error.
///
- public static string TaskError {
+ public static string TaskErrorButton {
get {
- return ResourceManager.GetString("TaskError", resourceCulture);
+ return ResourceManager.GetString("TaskErrorButton", resourceCulture);
}
}
@@ -1805,6 +1814,15 @@ public static string UnableToParseLinkMessage {
}
}
+ ///
+ /// Looks up a localized string similar to Unable to start Windows application theme watcher. Error code: {0}.
+ ///
+ public static string UnableToStartWindowsThemeWatcher {
+ get {
+ return ResourceManager.GetString("UnableToStartWindowsThemeWatcher", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Unknown.
///
diff --git a/TwitchDownloaderWPF/Translations/Strings.es.resx b/TwitchDownloaderWPF/Translations/Strings.es.resx
index 652ddcd2..9a010065 100644
--- a/TwitchDownloaderWPF/Translations/Strings.es.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.es.resx
@@ -470,7 +470,7 @@
Cancelar
-
+
Error
@@ -766,4 +766,10 @@
Codificar metadatos:
+
+ Error
+
+
+ Unable to start Windows application theme watcher. Error code: {0}
+
diff --git a/TwitchDownloaderWPF/Translations/Strings.fr.resx b/TwitchDownloaderWPF/Translations/Strings.fr.resx
index d9f89300..0fbdff6b 100644
--- a/TwitchDownloaderWPF/Translations/Strings.fr.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.fr.resx
@@ -470,7 +470,7 @@
Annulation
-
+
Erreur
@@ -765,4 +765,10 @@
Inclure les métadonnées
+
+ Erreur
+
+
+ Impossible de démarrer l'observateur de thème de l'application Windows. Code d'erreur : {0}
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.pl.resx b/TwitchDownloaderWPF/Translations/Strings.pl.resx
index 9ee24e8f..7fc6c681 100644
--- a/TwitchDownloaderWPF/Translations/Strings.pl.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.pl.resx
@@ -470,7 +470,7 @@
Anuluj
-
+
Błąd
@@ -765,4 +765,10 @@
Encode Metadata:
+
+ Błąd
+
+
+ Unable to start Windows application theme watcher. Error code: {0}
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.resx b/TwitchDownloaderWPF/Translations/Strings.resx
index 426d4b59..112829f2 100644
--- a/TwitchDownloaderWPF/Translations/Strings.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.resx
@@ -470,7 +470,7 @@
Cancel
-
+
Error
@@ -764,4 +764,10 @@
Encode Metadata:
+
+ Error
+
+
+ Unable to start Windows application theme watcher. Error code: {0}
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.ru.resx b/TwitchDownloaderWPF/Translations/Strings.ru.resx
index 1d50d93b..06fb8a90 100644
--- a/TwitchDownloaderWPF/Translations/Strings.ru.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.ru.resx
@@ -470,7 +470,7 @@
Отмена
-
+
Ошибка
@@ -765,4 +765,10 @@
Encode Metadata:
+
+ Ошибка
+
+
+ Unable to start Windows application theme watcher. Error code: {0}
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.tr.resx b/TwitchDownloaderWPF/Translations/Strings.tr.resx
index f752f139..4b1240e1 100644
--- a/TwitchDownloaderWPF/Translations/Strings.tr.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.tr.resx
@@ -471,7 +471,7 @@
İptal
-
+
Hata
@@ -766,4 +766,10 @@
Encode Metadata:
+
+ Hata
+
+
+ Unable to start Windows application theme watcher. Error code: {0}
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.zh.resx b/TwitchDownloaderWPF/Translations/Strings.zh.resx
index 2f964568..b9d0cad1 100644
--- a/TwitchDownloaderWPF/Translations/Strings.zh.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.zh.resx
@@ -470,7 +470,7 @@
取消
-
+
错误
@@ -764,4 +764,10 @@
Encode Metadata:
+
+ 错误
+
+
+ Unable to start Windows application theme watcher. Error code: {0}
+
\ No newline at end of file
From eb601f814e6e17c276d2aa68c8fd1eb58a885f8f Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Wed, 23 Aug 2023 02:20:35 -0400
Subject: [PATCH 05/50] Do not render sub/highlight backgrounds in transparent
renders (#799)
* Compile PayingForward & ChannelPointHighlight colors as SKColors instead of using SKColor.Parse at runtime.
* Do not draw subscribe/misc highlight background if the message background is not opaque enough.
* Fix boolean logic
---
TwitchDownloaderCore/ChatRenderer.cs | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/TwitchDownloaderCore/ChatRenderer.cs b/TwitchDownloaderCore/ChatRenderer.cs
index 9ffe12d4..adca1ed5 100644
--- a/TwitchDownloaderCore/ChatRenderer.cs
+++ b/TwitchDownloaderCore/ChatRenderer.cs
@@ -28,7 +28,6 @@ public sealed class ChatRenderer : IDisposable
private const string PURPLE = "#7B2CF2";
private static readonly SKColor Purple = SKColor.Parse(PURPLE);
- private static readonly SKColor HighlightBackground = SKColor.Parse("#1A6B6B6E");
private static readonly string[] DefaultUsernameColors = { "#FF0000", "#0000FF", "#00FF00", "#B22222", "#FF7F50", "#9ACD32", "#FF4500", "#2E8B57", "#DAA520", "#D2691E", "#5F9EA0", "#1E90FF", "#FF69B4", "#8A2BE2", "#00FF7F" };
private static readonly Regex RtlRegex = new("[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]", RegexOptions.Compiled);
@@ -617,15 +616,26 @@ private SKBitmap CombineImages(List<(SKImageInfo info, SKBitmap bitmap)> section
{
if (highlightType is HighlightType.PayingForward or HighlightType.ChannelPointHighlight)
{
- var colorString = highlightType is HighlightType.PayingForward ? "#26262C" : "#80808C";
- using var paint = new SKPaint { Color = SKColor.Parse(colorString) };
+ var accentColor = highlightType is HighlightType.PayingForward
+ ? new SKColor(0x26, 0x26, 0x2C, 0xFF) // #26262C (RRGGBB)
+ : new SKColor(0x80, 0x80, 0x8C, 0xFF); // #80808C (RRGGBB)
+
+ using var paint = new SKPaint { Color = accentColor };
finalCanvas.DrawRect(renderOptions.SidePadding, 0, renderOptions.AccentStrokeWidth, finalBitmapInfo.Height, paint);
}
else if (highlightType is not HighlightType.None)
{
- using var backgroundPaint = new SKPaint { Color = HighlightBackground };
+ const int OPAQUE_THRESHOLD = 245;
+ if (!(renderOptions.BackgroundColor.Alpha < OPAQUE_THRESHOLD ||
+ (renderOptions.AlternateMessageBackgrounds && renderOptions.AlternateBackgroundColor.Alpha < OPAQUE_THRESHOLD)))
+ {
+ // Draw the highlight background only if the message background is opaque enough
+ var backgroundColor = new SKColor(0x6B, 0x6B, 0x6E, 0x1A); // #1A6B6B6E (AARRGGBB)
+ using var backgroundPaint = new SKPaint { Color = backgroundColor };
+ finalCanvas.DrawRect(renderOptions.SidePadding, 0, finalBitmapInfo.Width - renderOptions.SidePadding * 2, finalBitmapInfo.Height, backgroundPaint);
+ }
+
using var accentPaint = new SKPaint { Color = Purple };
- finalCanvas.DrawRect(renderOptions.SidePadding, 0, finalBitmapInfo.Width - renderOptions.SidePadding * 2, finalBitmapInfo.Height, backgroundPaint);
finalCanvas.DrawRect(renderOptions.SidePadding, 0, renderOptions.AccentStrokeWidth, finalBitmapInfo.Height, accentPaint);
}
From e6f957cf735dc6e1e3ce023954c913db863cf8fa Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Wed, 30 Aug 2023 02:03:12 -0400
Subject: [PATCH 06/50] WPF Chat Updater Fatal Error Fixes (#803)
* Do not enable chatupdater buttons until chat json is fully loaded & catch deserialization exceptions.
Fixes a crash when loading large chats
* Catch potential exceptions when setting up chatupdater video/clip info & fix potential for thumbnail image to not update between chats
* Fix potential false positive success from TryGetThumb
* Document exceptions from ChatJson.DeserializeAsync
---
TwitchDownloaderCore/Chat/ChatJson.cs | 2 +
TwitchDownloaderWPF/PageChatUpdate.xaml.cs | 139 ++++++++++--------
.../Services/ThumbnailService.cs | 12 +-
3 files changed, 89 insertions(+), 64 deletions(-)
diff --git a/TwitchDownloaderCore/Chat/ChatJson.cs b/TwitchDownloaderCore/Chat/ChatJson.cs
index a0f67526..d301921e 100644
--- a/TwitchDownloaderCore/Chat/ChatJson.cs
+++ b/TwitchDownloaderCore/Chat/ChatJson.cs
@@ -24,6 +24,8 @@ public static class ChatJson
/// Asynchronously deserializes a chat json file.
///
/// A representation the deserialized chat json file.
+ /// The file does not exist.
+ /// The file is not a valid chat format.
public static async Task DeserializeAsync(string filePath, bool getComments = true, bool getEmbeds = true, CancellationToken cancellationToken = new())
{
ArgumentNullException.ThrowIfNull(filePath, nameof(filePath));
diff --git a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
index 1650595e..2a064828 100644
--- a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
+++ b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
@@ -53,16 +53,35 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e)
textJson.Text = openFileDialog.FileName;
InputFile = openFileDialog.FileName;
- SetEnabled(true);
+ ChatJsonInfo = null;
+ imgThumbnail.Source = null;
+ SetEnabled(false);
if (Path.GetExtension(InputFile)!.ToLower() is not ".json" and not ".gz")
{
+ textJson.Text = "";
+ InputFile = "";
+ return;
+ }
+
+ try
+ {
+ ChatJsonInfo = await ChatJson.DeserializeAsync(InputFile, true, false, CancellationToken.None);
+ ChatJsonInfo.comments.RemoveRange(1, ChatJsonInfo.comments.Count - 2);
+ GC.Collect();
+ }
+ catch (Exception ex)
+ {
+ AppendLog(Translations.Strings.ErrorLog + ex.Message);
+ if (Settings.Default.VerboseErrors)
+ {
+ MessageBox.Show(ex.ToString(), Translations.Strings.VerboseErrorOutput, MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+
return;
}
- ChatJsonInfo = await ChatJson.DeserializeAsync(InputFile, true, false, CancellationToken.None);
- ChatJsonInfo.comments.RemoveRange(1, ChatJsonInfo.comments.Count - 2);
- GC.Collect();
+ SetEnabled(true);
var videoCreatedAt = ChatJsonInfo.video.created_at == default
? ChatJsonInfo.comments[0].created_at - TimeSpan.FromSeconds(ChatJsonInfo.comments[0].content_offset_seconds)
@@ -92,85 +111,83 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e)
ViewCount = ChatJsonInfo.video.viewCount;
Game = ChatJsonInfo.video.game ?? ChatJsonInfo.video.chapters.FirstOrDefault()?.gameDisplayName ?? "Unknown";
- if (VideoId.All(char.IsDigit))
+ try
{
- GqlVideoResponse videoInfo = await TwitchHelper.GetVideoInfo(int.Parse(VideoId));
- if (videoInfo.data.video == null)
+ if (VideoId.All(char.IsDigit))
{
- AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail + ": " + Translations.Strings.VodExpiredOrIdCorrupt);
- var (success, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
- if (success)
+ GqlVideoResponse videoInfo = await TwitchHelper.GetVideoInfo(int.Parse(VideoId));
+ if (videoInfo.data.video == null)
{
+ AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail + ": " + Translations.Strings.VodExpiredOrIdCorrupt);
+ var (_, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
imgThumbnail.Source = image;
+
+ numStartHour.Maximum = 48;
+ numEndHour.Maximum = 48;
}
- numStartHour.Maximum = 48;
- numEndHour.Maximum = 48;
- }
- else
- {
- VideoLength = TimeSpan.FromSeconds(videoInfo.data.video.lengthSeconds);
- labelLength.Text = VideoLength.ToString("c");
- numStartHour.Maximum = (int)VideoLength.TotalHours;
- numEndHour.Maximum = (int)VideoLength.TotalHours;
- ViewCount = videoInfo.data.video.viewCount;
- Game = videoInfo.data.video.game?.displayName;
-
- try
- {
- string thumbUrl = videoInfo.data.video.thumbnailURLs.FirstOrDefault();
- imgThumbnail.Source = await ThumbnailService.GetThumb(thumbUrl);
- }
- catch
+ else
{
- AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
- var (success, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
- if (success)
+ VideoLength = TimeSpan.FromSeconds(videoInfo.data.video.lengthSeconds);
+ labelLength.Text = VideoLength.ToString("c");
+ numStartHour.Maximum = (int)VideoLength.TotalHours;
+ numEndHour.Maximum = (int)VideoLength.TotalHours;
+ ViewCount = videoInfo.data.video.viewCount;
+ Game = videoInfo.data.video.game?.displayName;
+
+ var thumbUrl = videoInfo.data.video.thumbnailURLs.FirstOrDefault();
+ var (success, image) = await ThumbnailService.TryGetThumb(thumbUrl);
+ if (!success)
{
- imgThumbnail.Source = image;
+ AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
+ (_, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
}
- }
- }
- }
- else
- {
- if (VideoId != "-1")
- {
- numStartHour.Maximum = 0;
- numEndHour.Maximum = 0;
- }
- GqlClipResponse videoInfo = await TwitchHelper.GetClipInfo(VideoId);
- if (videoInfo.data.clip.video == null)
- {
- AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail + ": " + Translations.Strings.VodExpiredOrIdCorrupt);
- var (success, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
- if (success)
- {
+
imgThumbnail.Source = image;
}
}
else
{
- VideoLength = TimeSpan.FromSeconds(videoInfo.data.clip.durationSeconds);
- labelLength.Text = VideoLength.ToString("c");
- ViewCount = videoInfo.data.clip.viewCount;
- Game = videoInfo.data.clip.game?.displayName;
+ if (VideoId != "-1")
+ {
+ numStartHour.Maximum = 0;
+ numEndHour.Maximum = 0;
+ }
- try
+ GqlClipResponse videoInfo = await TwitchHelper.GetClipInfo(VideoId);
+ if (videoInfo.data.clip.video == null)
{
- string thumbUrl = videoInfo.data.clip.thumbnailURL;
- imgThumbnail.Source = await ThumbnailService.GetThumb(thumbUrl);
+ AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail + ": " + Translations.Strings.VodExpiredOrIdCorrupt);
+ var (_, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
+ imgThumbnail.Source = image;
}
- catch
+ else
{
- AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
- var (success, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
- if (success)
+ VideoLength = TimeSpan.FromSeconds(videoInfo.data.clip.durationSeconds);
+ labelLength.Text = VideoLength.ToString("c");
+ ViewCount = videoInfo.data.clip.viewCount;
+ Game = videoInfo.data.clip.game?.displayName;
+
+ var thumbUrl = videoInfo.data.clip.thumbnailURL;
+ var (success, image) = await ThumbnailService.TryGetThumb(thumbUrl);
+ if (!success)
{
- imgThumbnail.Source = image;
+ AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
+ (_, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
}
+
+ imgThumbnail.Source = image;
}
}
}
+ catch (Exception ex)
+ {
+ MessageBox.Show(Translations.Strings.UnableToGetInfoMessage, Translations.Strings.UnableToGetInfo, MessageBoxButton.OK, MessageBoxImage.Error);
+ AppendLog(Translations.Strings.ErrorLog + ex.Message);
+ if (Settings.Default.VerboseErrors)
+ {
+ MessageBox.Show(ex.ToString(), Translations.Strings.VerboseErrorOutput, MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
}
private void UpdateActionButtons(bool isUpdating)
diff --git a/TwitchDownloaderWPF/Services/ThumbnailService.cs b/TwitchDownloaderWPF/Services/ThumbnailService.cs
index 9d960af1..102b4445 100644
--- a/TwitchDownloaderWPF/Services/ThumbnailService.cs
+++ b/TwitchDownloaderWPF/Services/ThumbnailService.cs
@@ -7,14 +7,19 @@ namespace TwitchDownloaderWPF.Services
public static class ThumbnailService
{
public const string THUMBNAIL_MISSING_URL = @"https://vod-secure.twitch.tv/_404/404_processing_320x180.png";
- private static readonly HttpClient _httpClient = new();
+ private static readonly HttpClient HttpClient = new();
public static async Task GetThumb(string thumbUrl)
{
+ if (string.IsNullOrWhiteSpace(thumbUrl))
+ {
+ return null;
+ }
+
BitmapImage img = new BitmapImage();
img.CacheOption = BitmapCacheOption.OnLoad;
img.BeginInit();
- img.StreamSource = await _httpClient.GetStreamAsync(thumbUrl);
+ img.StreamSource = await HttpClient.GetStreamAsync(thumbUrl);
img.EndInit();
return img;
}
@@ -23,7 +28,8 @@ public static async Task GetThumb(string thumbUrl)
{
try
{
- return (true, await GetThumb(thumbUrl));
+ var thumb = await GetThumb(thumbUrl);
+ return (thumb != null, thumb);
}
catch
{
From 69a66da9e563e15d96dc2666df807ee755206ccc Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Wed, 30 Aug 2023 02:37:35 -0400
Subject: [PATCH 07/50] Videodownload 99% stop fix (#792)
* Delay ThrottledStream stopwatch creation to first read
* Fix incorrect step for Finalizing Video
* Use CancellationTokenSource timers to cancel DownloadFileAsync after a minimum amount of time
* Bump timeout from 30 seconds to 60
* Add comment explaining CTS trickery
* Increase maxRestartedThreads
---
TwitchDownloaderCore/Tools/ThrottledStream.cs | 6 +-
TwitchDownloaderCore/VideoDownloader.cs | 134 +++++++++++++-----
2 files changed, 102 insertions(+), 38 deletions(-)
diff --git a/TwitchDownloaderCore/Tools/ThrottledStream.cs b/TwitchDownloaderCore/Tools/ThrottledStream.cs
index e5c4bb32..087986a9 100644
--- a/TwitchDownloaderCore/Tools/ThrottledStream.cs
+++ b/TwitchDownloaderCore/Tools/ThrottledStream.cs
@@ -11,8 +11,8 @@ public class ThrottledStream : Stream
{
public readonly Stream BaseStream;
public readonly int MaximumBytesPerSecond;
- private readonly Stopwatch _watch = Stopwatch.StartNew();
- private long _totalBytesRead = 0;
+ private Stopwatch _watch;
+ private long _totalBytesRead;
///
/// Initializes a new instance of the class
@@ -79,6 +79,8 @@ private async Task GetBytesToReturnAsync(int count)
if (MaximumBytesPerSecond <= 0)
return count;
+ _watch ??= Stopwatch.StartNew();
+
var canSend = (long)(_watch.ElapsedMilliseconds * (MaximumBytesPerSecond / 1000.0));
var diff = (int)(canSend - _totalBytesRead);
diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs
index cc0b7976..43f080fc 100644
--- a/TwitchDownloaderCore/VideoDownloader.cs
+++ b/TwitchDownloaderCore/VideoDownloader.cs
@@ -56,7 +56,7 @@ public async Task DownloadAsync(CancellationToken cancellationToken)
GqlVideoChapterResponse videoChapterResponse = await TwitchHelper.GetVideoChapters(downloadOptions.Id);
var (playlistUrl, bandwidth) = await GetPlaylistUrl();
- string baseUrl = playlistUrl.Substring(0, playlistUrl.LastIndexOf('/') + 1);
+ var baseUrl = new Uri(playlistUrl[..(playlistUrl.LastIndexOf('/') + 1)], UriKind.Absolute);
var videoLength = TimeSpan.FromSeconds(videoInfoResponse.data.video.lengthSeconds);
CheckAvailableStorageSpace(bandwidth, videoLength);
@@ -165,7 +165,7 @@ private void CheckAvailableStorageSpace(int bandwidth, TimeSpan videoLength)
}
}
- private async Task DownloadVideoPartsAsync(List videoPartsList, string baseUrl, string downloadFolder, double vodAge, CancellationToken cancellationToken)
+ private async Task DownloadVideoPartsAsync(List videoPartsList, Uri baseUrl, string downloadFolder, double vodAge, CancellationToken cancellationToken)
{
var partCount = videoPartsList.Count;
var videoPartsQueue = new ConcurrentQueue(videoPartsList);
@@ -181,33 +181,68 @@ private async Task DownloadVideoPartsAsync(List videoPartsList, string b
LogDownloadThreadExceptions(downloadExceptions);
}
- private Task StartNewDownloadThread(ConcurrentQueue videoPartsQueue, string baseUrl, string downloadFolder, double vodAge, CancellationToken cancellationToken)
+ private Task StartNewDownloadThread(ConcurrentQueue videoPartsQueue, Uri baseUrl, string downloadFolder, double vodAge, CancellationToken cancellationToken)
{
- return Task.Factory.StartNew(state =>
+ return Task.Factory.StartNew(
+ ExecuteDownloadThread,
+ new Tuple, HttpClient, Uri, string, double, int, CancellationToken>(
+ videoPartsQueue, _httpClient, baseUrl, downloadFolder, vodAge, downloadOptions.ThrottleKib, cancellationToken),
+ cancellationToken,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Current);
+
+ static void ExecuteDownloadThread(object state)
+ {
+ var (partQueue, httpClient, rootUrl, cacheFolder, videoAge, throttleKib, cancelToken) =
+ (Tuple, HttpClient, Uri, string, double, int, CancellationToken>)state;
+
+ using var cts = new CancellationTokenSource();
+ cancelToken.Register(PropagateCancel, cts);
+
+ while (!partQueue.IsEmpty)
{
- var (partQueue, rootUrl, cacheFolder, videoAge, throttleKib, cancelToken) =
- (Tuple, string, string, double, int, CancellationToken>)state;
+ cancelToken.ThrowIfCancellationRequested();
- while (!partQueue.IsEmpty)
+ string videoPart = null;
+ try
+ {
+ if (partQueue.TryDequeue(out videoPart))
+ {
+ DownloadVideoPartAsync(httpClient, rootUrl, videoPart, cacheFolder, videoAge, throttleKib, cts).GetAwaiter().GetResult();
+ }
+ }
+ catch
{
- if (partQueue.TryDequeue(out var request))
+ if (videoPart != null && !cancelToken.IsCancellationRequested)
{
- DownloadVideoPartAsync(rootUrl, request, cacheFolder, videoAge, throttleKib, cancelToken).GetAwaiter().GetResult();
+ // Requeue the video part now instead of deferring to the verifier since we already know it's bad
+ partQueue.Enqueue(videoPart);
}
- Task.Delay(77, cancelToken).GetAwaiter().GetResult();
+ throw;
}
- }, new Tuple, string, string, double, int, CancellationToken>(
- videoPartsQueue, baseUrl, downloadFolder, vodAge, downloadOptions.ThrottleKib, cancellationToken),
- cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
+
+ const int aPrimeNumber = 71;
+ Thread.Sleep(aPrimeNumber);
+ }
+ }
+
+ static void PropagateCancel(object tokenSourceToCancel)
+ {
+ try
+ {
+ ((CancellationTokenSource)tokenSourceToCancel)?.Cancel();
+ }
+ catch (ObjectDisposedException) { }
+ }
}
- private async Task> WaitForDownloadThreads(Task[] tasks, ConcurrentQueue videoPartsQueue, string baseUrl, string downloadFolder, double vodAge, int partCount, CancellationToken cancellationToken)
+ private async Task> WaitForDownloadThreads(Task[] tasks, ConcurrentQueue videoPartsQueue, Uri baseUrl, string downloadFolder, double vodAge, int partCount, CancellationToken cancellationToken)
{
var allThreadsExited = false;
var previousDoneCount = 0;
var restartedThreads = 0;
- var maxRestartedThreads = Math.Max(downloadOptions.DownloadThreads, 10);
+ var maxRestartedThreads = (int)Math.Max(downloadOptions.DownloadThreads * 1.5, 10);
var downloadExceptions = new Dictionary();
do
{
@@ -249,17 +284,17 @@ private async Task> WaitForDownloadThreads(Task[] tas
throw new AggregateException("The download thread restart limit was reached.", downloadExceptions.Values);
}
- return downloadExceptions;
+ return downloadExceptions.Values;
}
- private void LogDownloadThreadExceptions(Dictionary downloadExceptions)
+ private void LogDownloadThreadExceptions(IReadOnlyCollection downloadExceptions)
{
if (downloadExceptions.Count == 0)
return;
var culpritList = new List();
var sb = new StringBuilder();
- foreach (var downloadException in downloadExceptions.Values)
+ foreach (var downloadException in downloadExceptions)
{
var ex = downloadException switch
{
@@ -289,7 +324,7 @@ private void LogDownloadThreadExceptions(Dictionary downloadExce
_progress.Report(new ProgressReport(ReportType.Log, sb.ToString()));
}
- private async Task VerifyDownloadedParts(List videoParts, string baseUrl, string downloadFolder, double vodAge, CancellationToken cancellationToken)
+ private async Task VerifyDownloadedParts(List videoParts, Uri baseUrl, string downloadFolder, double vodAge, CancellationToken cancellationToken)
{
var failedParts = new List();
var partCount = videoParts.Count;
@@ -408,34 +443,35 @@ private static void HandleFfmpegOutput(string output, Regex encodingTimeRegex, d
// Apparently it is possible for the percent to not be within the range of 0-100. lay295#716
if (percent is < 0 or > 100)
{
- progress.Report(new ProgressReport(ReportType.SameLineStatus, "Finalizing Video... [4/4]"));
+ progress.Report(new ProgressReport(ReportType.SameLineStatus, "Finalizing Video... [5/5]"));
progress.Report(new ProgressReport(0));
}
else
{
- progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Finalizing Video {percent}% [4/4]"));
+ progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Finalizing Video {percent}% [5/5]"));
progress.Report(new ProgressReport(percent));
}
}
- private async Task DownloadVideoPartAsync(string baseUrl, string videoPartName, string downloadFolder, double vodAge, int throttleKib, CancellationToken cancellationToken)
+ /// The may be canceled by this method.
+ private static async Task DownloadVideoPartAsync(HttpClient httpClient, Uri baseUrl, string videoPartName, string downloadFolder, double vodAge, int throttleKib, CancellationTokenSource cancellationTokenSource)
{
bool tryUnmute = vodAge < 24;
int errorCount = 0;
int timeoutCount = 0;
while (true)
{
- cancellationToken.ThrowIfCancellationRequested();
+ cancellationTokenSource.Token.ThrowIfCancellationRequested();
try
{
if (tryUnmute && videoPartName.Contains("-muted"))
{
- await DownloadFileTaskAsync(baseUrl + videoPartName.Replace("-muted", ""), Path.Combine(downloadFolder, RemoveQueryString(videoPartName)), throttleKib, cancellationToken);
+ await DownloadFileAsync(httpClient, new Uri(baseUrl, videoPartName.Replace("-muted", "")), Path.Combine(downloadFolder, RemoveQueryString(videoPartName)), throttleKib, cancellationTokenSource);
}
else
{
- await DownloadFileTaskAsync(baseUrl + videoPartName, Path.Combine(downloadFolder, RemoveQueryString(videoPartName)), throttleKib, cancellationToken);
+ await DownloadFileAsync(httpClient, new Uri(baseUrl, videoPartName), Path.Combine(downloadFolder, RemoveQueryString(videoPartName)), throttleKib, cancellationTokenSource);
}
return;
@@ -446,21 +482,23 @@ private async Task DownloadVideoPartAsync(string baseUrl, string videoPartName,
}
catch (HttpRequestException)
{
- if (++errorCount > 10)
+ const int maxRetries = 10;
+ if (++errorCount > maxRetries)
{
- throw new HttpRequestException($"Video part {videoPartName} failed after 10 retries");
+ throw new HttpRequestException($"Video part {videoPartName} failed after {maxRetries} retries");
}
- await Task.Delay(1_000 * errorCount, cancellationToken);
+ await Task.Delay(1_000 * errorCount, cancellationTokenSource.Token);
}
catch (TaskCanceledException ex) when (ex.Message.Contains("HttpClient.Timeout"))
{
- if (++timeoutCount > 3)
+ const int maxRetries = 3;
+ if (++timeoutCount > maxRetries)
{
- throw new HttpRequestException($"Video part {videoPartName} timed out 3 times");
+ throw new HttpRequestException($"Video part {videoPartName} timed out {maxRetries} times");
}
- await Task.Delay(5_000 * timeoutCount, cancellationToken);
+ await Task.Delay(5_000 * timeoutCount, cancellationTokenSource.Token);
}
}
}
@@ -558,17 +596,36 @@ private async Task DownloadVideoPartAsync(string baseUrl, string videoPartName,
///
/// Downloads the requested to the without storing it in memory.
///
+ /// The to perform the download operation.
/// The url of the file to download.
/// The path to the file where download will be saved.
/// The maximum download speed in kibibytes per second, or -1 for no maximum.
- /// The cancellation token to cancel the operation.
- private async Task DownloadFileTaskAsync(string url, string destinationFile, int throttleKib, CancellationToken cancellationToken = default)
+ /// A containing a to cancel the operation.
+ /// The may be canceled by this method.
+ private static async Task DownloadFileAsync(HttpClient httpClient, Uri url, string destinationFile, int throttleKib, CancellationTokenSource cancellationTokenSource = null)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
- using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None;
+
+ using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
+ // Why are we setting a CTS CancelAfter timer? See lay295#265
+ const int sixtySeconds = 60;
+ if (throttleKib == -1 || !response.Content.Headers.ContentLength.HasValue)
+ {
+ cancellationTokenSource?.CancelAfter(TimeSpan.FromSeconds(sixtySeconds));
+ }
+ else
+ {
+ const double oneKibibyte = 1024d;
+ cancellationTokenSource?.CancelAfter(TimeSpan.FromSeconds(Math.Max(
+ sixtySeconds,
+ response.Content.Headers.ContentLength!.Value / oneKibibyte / throttleKib * 8 // Allow up to 8x the shortest download time given the thread bandwidth
+ )));
+ }
+
switch (throttleKib)
{
case -1:
@@ -581,13 +638,14 @@ private async Task DownloadFileTaskAsync(string url, string destinationFile, int
{
try
{
- await using var throttledStream = new ThrottledStream(await response.Content.ReadAsStreamAsync(cancellationToken), throttleKib);
+ await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
+ await using var throttledStream = new ThrottledStream(contentStream, throttleKib);
await using var fs = new FileStream(destinationFile, FileMode.Create, FileAccess.Write, FileShare.Read);
await throttledStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
catch (IOException e) when (e.Message.Contains("EOF"))
{
- // The throttled stream throws when it reads an unexpected EOF, try again without the limiter
+ // If we get an exception for EOF, it may be related to the throttler. Try again without it.
// TODO: Log this somehow
await Task.Delay(2_000, cancellationToken);
goto case -1;
@@ -595,6 +653,10 @@ private async Task DownloadFileTaskAsync(string url, string destinationFile, int
break;
}
}
+
+ // Reset the cts timer so it can be reused for the next download on this thread.
+ // Is there a friendlier way to do this? Yes. Does it involve creating and destroying 4,000 CancellationTokenSources that are almost never cancelled? Also Yes.
+ cancellationTokenSource?.CancelAfter(TimeSpan.FromMilliseconds(uint.MaxValue - 1));
}
private async Task CombineVideoParts(string downloadFolder, List videoParts, CancellationToken cancellationToken)
From 23711924c3b7b7f7a1bb520542fd3f4308a1f98d Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 1 Sep 2023 19:38:51 -0400
Subject: [PATCH 08/50] Fix CLI FFmpeg download percentage
---
TwitchDownloaderCLI/Tools/FfmpegHandler.cs | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/TwitchDownloaderCLI/Tools/FfmpegHandler.cs b/TwitchDownloaderCLI/Tools/FfmpegHandler.cs
index 003628ab..5be21fa3 100644
--- a/TwitchDownloaderCLI/Tools/FfmpegHandler.cs
+++ b/TwitchDownloaderCLI/Tools/FfmpegHandler.cs
@@ -25,7 +25,7 @@ private static void DownloadFfmpeg()
Console.Write("[INFO] - Downloading FFmpeg");
var progressHandler = new Progress();
- progressHandler.ProgressChanged += XabeProgressHandler.OnProgressReceived;
+ progressHandler.ProgressChanged += new XabeProgressHandler().OnProgressReceived;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@@ -64,12 +64,19 @@ public static void DetectFfmpeg(string ffmpegPath)
}
}
- internal static class XabeProgressHandler
+ internal class XabeProgressHandler
{
- internal static void OnProgressReceived(object sender, ProgressInfo e)
+ private int _lastPercent = -1;
+
+ internal void OnProgressReceived(object sender, ProgressInfo e)
{
var percent = (int)(e.DownloadedBytes / (double)e.TotalBytes * 100);
- Console.Write($"\r[INFO] - Downloading FFmpeg {percent}%");
+
+ if (percent > _lastPercent)
+ {
+ _lastPercent = percent;
+ Console.Write($"\r[INFO] - Downloading FFmpeg {percent}%");
+ }
}
}
}
\ No newline at end of file
From f819572d1d0fae6b4514af79cd5a26d149f43c10 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 1 Sep 2023 21:09:10 -0400
Subject: [PATCH 09/50] Cleanup GetThirdPartyEmotes
---
TwitchDownloaderCore/TwitchHelper.cs | 83 ++++++++++------------------
1 file changed, 30 insertions(+), 53 deletions(-)
diff --git a/TwitchDownloaderCore/TwitchHelper.cs b/TwitchDownloaderCore/TwitchHelper.cs
index 23a54736..d8c4a820 100644
--- a/TwitchDownloaderCore/TwitchHelper.cs
+++ b/TwitchDownloaderCore/TwitchHelper.cs
@@ -319,82 +319,59 @@ private static async Task GetStvEmoteData(int streamerId, List Regex.IsMatch(comment.message.body, pattern))
- select emote;
-
- foreach (var emote in emoteResponseItemsQuery)
- {
- try
- {
- TwitchEmote newEmote = new TwitchEmote(await GetImage(bttvFolder, emote.ImageUrl.Replace("[scale]", "2"), emote.Id, "2", emote.ImageType, cancellationToken), EmoteProvider.ThirdParty, 2, emote.Id, emote.Code);
- if (emote.IsZeroWidth)
- newEmote.IsZeroWidth = true;
- returnList.Add(newEmote);
- alreadyAdded.Add(emote.Code);
- }
- catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
- }
+ await FetchEmoteImages(comments, emoteDataResponse.BTTV, returnList, alreadyAdded, bttvFolder, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
if (ffz)
{
- if (!Directory.Exists(ffzFolder))
- TwitchHelper.CreateDirectory(ffzFolder);
-
- var emoteResponseItemsQuery = from emote in emoteDataResponse.FFZ
- where !alreadyAdded.Contains(emote.Code)
- let pattern = $@"(?<=^|\s){Regex.Escape(emote.Code)}(?=$|\s)"
- where comments.Any(comment => Regex.IsMatch(comment.message.body, pattern))
- select emote;
-
- foreach (var emote in emoteResponseItemsQuery)
- {
- try
- {
- TwitchEmote newEmote = new TwitchEmote(await GetImage(ffzFolder, emote.ImageUrl.Replace("[scale]", "2"), emote.Id, "2", emote.ImageType, cancellationToken), EmoteProvider.ThirdParty, 2, emote.Id, emote.Code);
- returnList.Add(newEmote);
- alreadyAdded.Add(emote.Code);
- }
- catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
- }
+ await FetchEmoteImages(comments, emoteDataResponse.FFZ, returnList, alreadyAdded, ffzFolder, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
if (stv)
{
- if (!Directory.Exists(stvFolder))
- TwitchHelper.CreateDirectory(stvFolder);
+ await FetchEmoteImages(comments, emoteDataResponse.STV, returnList, alreadyAdded, stvFolder, cancellationToken);
+ }
+
+ return returnList;
+
+ static async Task FetchEmoteImages(IReadOnlyCollection comments, IEnumerable emoteResponse, ICollection returnList,
+ ICollection alreadyAdded, string cacheFolder, CancellationToken cancellationToken)
+ {
+ if (!Directory.Exists(cacheFolder))
+ CreateDirectory(cacheFolder);
- var emoteResponseItemsQuery = from emote in emoteDataResponse.STV
- where !alreadyAdded.Contains(emote.Code)
- let pattern = $@"(?<=^|\s){Regex.Escape(emote.Code)}(?=$|\s)"
- where comments.Any(comment => Regex.IsMatch(comment.message.body, pattern))
- select emote;
+ IEnumerable emoteResponseQuery;
+ if (comments.Count == 0)
+ {
+ emoteResponseQuery = emoteResponse;
+ }
+ else
+ {
+ emoteResponseQuery = from emote in emoteResponse
+ where !alreadyAdded.Contains(emote.Code)
+ let pattern = $@"(?<=^|\s){Regex.Escape(emote.Code)}(?=$|\s)"
+ where comments.Any(comment => Regex.IsMatch(comment.message.body, pattern))
+ select emote;
+ }
- foreach (var emote in emoteResponseItemsQuery)
+ foreach (var emote in emoteResponseQuery)
{
try
{
- TwitchEmote newEmote = new TwitchEmote(await GetImage(stvFolder, emote.ImageUrl.Replace("[scale]", "2"), emote.Id, "2", emote.ImageType, cancellationToken), EmoteProvider.ThirdParty, 2, emote.Id, emote.Code);
- if (emote.IsZeroWidth)
- newEmote.IsZeroWidth = true;
+ var imageData = await GetImage(cacheFolder, emote.ImageUrl.Replace("[scale]", "2"), emote.Id, "2", emote.ImageType, cancellationToken);
+ var newEmote = new TwitchEmote(imageData, EmoteProvider.ThirdParty, 2, emote.Id, emote.Code);
+ newEmote.IsZeroWidth = emote.IsZeroWidth;
+
returnList.Add(newEmote);
alreadyAdded.Add(emote.Code);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
}
-
- return returnList;
}
public static async Task> GetEmotes(List comments, string cacheFolder, EmbeddedData embeddedData = null, bool offline = false, CancellationToken cancellationToken = default)
From 1910f64236da40825d40d8537446f36c92fb04f3 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 8 Sep 2023 23:14:07 -0400
Subject: [PATCH 10/50] Change signatures of GetServiceEmoteData & add
CancellationTokens
---
TwitchDownloaderCore/TwitchHelper.cs | 57 +++++++++++--------
.../TwitchObjects/EmoteResponse.cs | 12 ++--
.../TwitchObjects/EmoteResponseItem.cs | 8 +--
3 files changed, 38 insertions(+), 39 deletions(-)
diff --git a/TwitchDownloaderCore/TwitchHelper.cs b/TwitchDownloaderCore/TwitchHelper.cs
index d8c4a820..e8ee2720 100644
--- a/TwitchDownloaderCore/TwitchHelper.cs
+++ b/TwitchDownloaderCore/TwitchHelper.cs
@@ -123,83 +123,87 @@ public static async Task GetGqlClips(string channelName,
return await response.Content.ReadFromJsonAsync();
}
- public static async Task GetThirdPartyEmoteData(int streamerId, bool getBttv, bool getFfz, bool getStv, bool allowUnlistedEmotes, CancellationToken cancellationToken = new())
+ public static async Task GetThirdPartyEmoteData(int streamerId, bool getBttv, bool getFfz, bool getStv, bool allowUnlistedEmotes, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
- EmoteResponse emoteReponse = new();
+ EmoteResponse emoteResponse = new();
if (getBttv)
{
- await GetBttvEmoteData(streamerId, emoteReponse.BTTV);
+ emoteResponse.BTTV = await GetBttvEmoteData(streamerId, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
if (getFfz)
{
- await GetFfzEmoteData(streamerId, emoteReponse.FFZ);
+ emoteResponse.FFZ = await GetFfzEmoteData(streamerId, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
if (getStv)
{
- await GetStvEmoteData(streamerId, emoteReponse.STV, allowUnlistedEmotes);
+ emoteResponse.STV = await GetStvEmoteData(streamerId, allowUnlistedEmotes, cancellationToken);
}
- return emoteReponse;
+ return emoteResponse;
}
- private static async Task GetBttvEmoteData(int streamerId, List bttvResponse)
+ private static async Task> GetBttvEmoteData(int streamerId, CancellationToken cancellationToken)
{
var globalEmoteRequest = new HttpRequestMessage(HttpMethod.Get, new Uri("https://api.betterttv.net/3/cached/emotes/global", UriKind.Absolute));
- using var globalEmoteResponse = await httpClient.SendAsync(globalEmoteRequest, HttpCompletionOption.ResponseHeadersRead);
+ using var globalEmoteResponse = await httpClient.SendAsync(globalEmoteRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
globalEmoteResponse.EnsureSuccessStatusCode();
- var BTTV = await globalEmoteResponse.Content.ReadFromJsonAsync>();
+ var BTTV = await globalEmoteResponse.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken);
//Channel might not have BTTV emotes
try
{
var channelEmoteRequest = new HttpRequestMessage(HttpMethod.Get, new Uri($"https://api.betterttv.net/3/cached/users/twitch/{streamerId}", UriKind.Absolute));
- using var channelEmoteResponse = await httpClient.SendAsync(channelEmoteRequest, HttpCompletionOption.ResponseHeadersRead);
+ using var channelEmoteResponse = await httpClient.SendAsync(channelEmoteRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
channelEmoteResponse.EnsureSuccessStatusCode();
- var bttvChannel = await channelEmoteResponse.Content.ReadFromJsonAsync();
+ var bttvChannel = await channelEmoteResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
BTTV.AddRange(bttvChannel.channelEmotes);
BTTV.AddRange(bttvChannel.sharedEmotes);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
+ var returnList = new List();
foreach (var emote in BTTV)
{
string id = emote.id;
string name = emote.code;
string mime = emote.imageType;
string url = $"https://cdn.betterttv.net/emote/{id}/[scale]x";
- bttvResponse.Add(new EmoteResponseItem() { Id = id, Code = name, ImageType = mime, ImageUrl = url, IsZeroWidth = bttvZeroWidth.Contains(name) });
+ returnList.Add(new EmoteResponseItem() { Id = id, Code = name, ImageType = mime, ImageUrl = url, IsZeroWidth = bttvZeroWidth.Contains(name) });
}
+
+ return returnList;
}
- private static async Task GetFfzEmoteData(int streamerId, List ffzResponse)
+ private static async Task> GetFfzEmoteData(int streamerId, CancellationToken cancellationToken)
{
var globalEmoteRequest = new HttpRequestMessage(HttpMethod.Get, new Uri("https://api.betterttv.net/3/cached/frankerfacez/emotes/global", UriKind.Absolute));
- using var globalEmoteResponse = await httpClient.SendAsync(globalEmoteRequest, HttpCompletionOption.ResponseHeadersRead);
+ using var globalEmoteResponse = await httpClient.SendAsync(globalEmoteRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
globalEmoteResponse.EnsureSuccessStatusCode();
- var FFZ = await globalEmoteResponse.Content.ReadFromJsonAsync>();
+ var FFZ = await globalEmoteResponse.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken);
//Channel might not have FFZ emotes
try
{
var channelEmoteRequest = new HttpRequestMessage(HttpMethod.Get, new Uri($"https://api.betterttv.net/3/cached/frankerfacez/users/twitch/{streamerId}", UriKind.Absolute));
- using var channelEmoteResponse = await httpClient.SendAsync(channelEmoteRequest, HttpCompletionOption.ResponseHeadersRead);
+ using var channelEmoteResponse = await httpClient.SendAsync(channelEmoteRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
channelEmoteResponse.EnsureSuccessStatusCode();
- var channelEmotes = await channelEmoteResponse.Content.ReadFromJsonAsync>();
+ var channelEmotes = await channelEmoteResponse.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken);
FFZ.AddRange(channelEmotes);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
+ var returnList = new List();
foreach (var emote in FFZ)
{
string id = emote.id.ToString();
@@ -208,26 +212,28 @@ private static async Task GetFfzEmoteData(int streamerId, List stvResponse, bool allowUnlistedEmotes)
+ private static async Task> GetStvEmoteData(int streamerId, bool allowUnlistedEmotes, CancellationToken cancellationToken)
{
var globalEmoteRequest = new HttpRequestMessage(HttpMethod.Get, new Uri("https://7tv.io/v3/emote-sets/global", UriKind.Absolute));
- using var globalEmoteResponse = await httpClient.SendAsync(globalEmoteRequest, HttpCompletionOption.ResponseHeadersRead);
+ using var globalEmoteResponse = await httpClient.SendAsync(globalEmoteRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
globalEmoteResponse.EnsureSuccessStatusCode();
- var globalEmoteObject = await globalEmoteResponse.Content.ReadFromJsonAsync();
+ var globalEmoteObject = await globalEmoteResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
var stvEmotes = globalEmoteObject.emotes;
// Channel might not be registered on 7tv
try
{
var streamerEmoteRequest = new HttpRequestMessage(HttpMethod.Get, new Uri($"https://7tv.io/v3/users/twitch/{streamerId}", UriKind.Absolute));
- using var streamerEmoteResponse = await httpClient.SendAsync(streamerEmoteRequest, HttpCompletionOption.ResponseHeadersRead);
+ using var streamerEmoteResponse = await httpClient.SendAsync(streamerEmoteRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
streamerEmoteResponse.EnsureSuccessStatusCode();
- var streamerEmoteObject = await streamerEmoteResponse.Content.ReadFromJsonAsync();
+ var streamerEmoteObject = await streamerEmoteResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
// Channel might not have emotes setup
if (streamerEmoteObject.emote_set?.emotes != null)
{
@@ -236,6 +242,7 @@ private static async Task GetStvEmoteData(int streamerId, List();
foreach (var stvEmote in stvEmotes)
{
STVData emoteData = stvEmote.data;
@@ -274,9 +281,11 @@ private static async Task GetStvEmoteData(int streamerId, List> 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, CancellationToken cancellationToken = new())
diff --git a/TwitchDownloaderCore/TwitchObjects/EmoteResponse.cs b/TwitchDownloaderCore/TwitchObjects/EmoteResponse.cs
index 581796df..845a5b49 100644
--- a/TwitchDownloaderCore/TwitchObjects/EmoteResponse.cs
+++ b/TwitchDownloaderCore/TwitchObjects/EmoteResponse.cs
@@ -1,15 +1,11 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.Collections.Generic;
namespace TwitchDownloaderCore.TwitchObjects
{
public class EmoteResponse
{
- public List BTTV { get; set; } = new();
- public List FFZ { get; set; } = new();
- public List STV { get; set; } = new();
+ public List BTTV { get; set; }
+ public List FFZ { get; set; }
+ public List STV { get; set; }
}
}
diff --git a/TwitchDownloaderCore/TwitchObjects/EmoteResponseItem.cs b/TwitchDownloaderCore/TwitchObjects/EmoteResponseItem.cs
index a610fa26..e74436a8 100644
--- a/TwitchDownloaderCore/TwitchObjects/EmoteResponseItem.cs
+++ b/TwitchDownloaderCore/TwitchObjects/EmoteResponseItem.cs
@@ -1,10 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace TwitchDownloaderCore.TwitchObjects
+namespace TwitchDownloaderCore.TwitchObjects
{
public class EmoteResponseItem
{
From e3333e8e7c79449f9f0fadb7025a7fe228a34100 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 8 Sep 2023 23:15:44 -0400
Subject: [PATCH 11/50] Add Nullable annotation to Core & WPF
---
TwitchDownloaderCore/TwitchDownloaderCore.csproj | 1 +
TwitchDownloaderWPF/TwitchDownloaderWPF.csproj | 1 +
2 files changed, 2 insertions(+)
diff --git a/TwitchDownloaderCore/TwitchDownloaderCore.csproj b/TwitchDownloaderCore/TwitchDownloaderCore.csproj
index 27ee1ec3..c0f199fe 100644
--- a/TwitchDownloaderCore/TwitchDownloaderCore.csproj
+++ b/TwitchDownloaderCore/TwitchDownloaderCore.csproj
@@ -10,6 +10,7 @@
AnyCPU;x64
default
true
+ enable
diff --git a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
index 0095a00c..98a512a3 100644
--- a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
+++ b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
@@ -21,6 +21,7 @@
true
false
AnyCPU;x64
+ enable
icon.ico
From dd0b9e9a9e37ea45d11f5193b76e8a8a6763c659 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 8 Sep 2023 23:19:00 -0400
Subject: [PATCH 12/50] Rename GetServiceEmoteData -> GetServiceEmotesMetadata
---
TwitchDownloaderCore/TwitchHelper.cs | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/TwitchDownloaderCore/TwitchHelper.cs b/TwitchDownloaderCore/TwitchHelper.cs
index e8ee2720..fdbe006a 100644
--- a/TwitchDownloaderCore/TwitchHelper.cs
+++ b/TwitchDownloaderCore/TwitchHelper.cs
@@ -123,7 +123,7 @@ public static async Task GetGqlClips(string channelName,
return await response.Content.ReadFromJsonAsync();
}
- public static async Task GetThirdPartyEmoteData(int streamerId, bool getBttv, bool getFfz, bool getStv, bool allowUnlistedEmotes, CancellationToken cancellationToken = default)
+ public static async Task GetThirdPartyEmotesMetadata(int streamerId, bool getBttv, bool getFfz, bool getStv, bool allowUnlistedEmotes, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -131,27 +131,27 @@ public static async Task GetThirdPartyEmoteData(int streamerId, b
if (getBttv)
{
- emoteResponse.BTTV = await GetBttvEmoteData(streamerId, cancellationToken);
+ emoteResponse.BTTV = await GetBttvEmotesMetadata(streamerId, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
if (getFfz)
{
- emoteResponse.FFZ = await GetFfzEmoteData(streamerId, cancellationToken);
+ emoteResponse.FFZ = await GetFfzEmotesMetadata(streamerId, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
if (getStv)
{
- emoteResponse.STV = await GetStvEmoteData(streamerId, allowUnlistedEmotes, cancellationToken);
+ emoteResponse.STV = await GetStvEmotesMetadata(streamerId, allowUnlistedEmotes, cancellationToken);
}
return emoteResponse;
}
- private static async Task> GetBttvEmoteData(int streamerId, CancellationToken cancellationToken)
+ private static async Task> GetBttvEmotesMetadata(int streamerId, CancellationToken cancellationToken)
{
var globalEmoteRequest = new HttpRequestMessage(HttpMethod.Get, new Uri("https://api.betterttv.net/3/cached/emotes/global", UriKind.Absolute));
using var globalEmoteResponse = await httpClient.SendAsync(globalEmoteRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
@@ -184,7 +184,7 @@ private static async Task> GetBttvEmoteData(int streamer
return returnList;
}
- private static async Task> GetFfzEmoteData(int streamerId, CancellationToken cancellationToken)
+ private static async Task> GetFfzEmotesMetadata(int streamerId, CancellationToken cancellationToken)
{
var globalEmoteRequest = new HttpRequestMessage(HttpMethod.Get, new Uri("https://api.betterttv.net/3/cached/frankerfacez/emotes/global", UriKind.Absolute));
using var globalEmoteResponse = await httpClient.SendAsync(globalEmoteRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
@@ -218,7 +218,7 @@ private static async Task> GetFfzEmoteData(int streamerI
return returnList;
}
- private static async Task> GetStvEmoteData(int streamerId, bool allowUnlistedEmotes, CancellationToken cancellationToken)
+ private static async Task> GetStvEmotesMetadata(int streamerId, bool allowUnlistedEmotes, CancellationToken cancellationToken)
{
var globalEmoteRequest = new HttpRequestMessage(HttpMethod.Get, new Uri("https://7tv.io/v3/emote-sets/global", UriKind.Absolute));
using var globalEmoteResponse = await httpClient.SendAsync(globalEmoteRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
@@ -324,7 +324,7 @@ private static async Task> GetStvEmoteData(int streamerI
string ffzFolder = Path.Combine(cacheFolder, "ffz");
string stvFolder = Path.Combine(cacheFolder, "stv");
- EmoteResponse emoteDataResponse = await GetThirdPartyEmoteData(streamerId, bttv, ffz, stv, allowUnlistedEmotes, cancellationToken);
+ EmoteResponse emoteDataResponse = await GetThirdPartyEmotesMetadata(streamerId, bttv, ffz, stv, allowUnlistedEmotes, cancellationToken);
if (bttv)
{
From 40069e7abf0a5b0560b39f3f23055f6f93e57d16 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 8 Sep 2023 23:27:49 -0400
Subject: [PATCH 13/50] Whoops, forgot to stage
---
TwitchDownloaderCore/Chat/ChatHtml.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/TwitchDownloaderCore/Chat/ChatHtml.cs b/TwitchDownloaderCore/Chat/ChatHtml.cs
index a4400fb3..1b2ccf61 100644
--- a/TwitchDownloaderCore/Chat/ChatHtml.cs
+++ b/TwitchDownloaderCore/Chat/ChatHtml.cs
@@ -88,7 +88,7 @@ public static class ChatHtml
private static async Task BuildThirdPartyDictionary(ChatRoot chatRoot, bool embedData, Dictionary thirdEmoteData, CancellationToken cancellationToken)
{
- EmoteResponse emotes = await TwitchHelper.GetThirdPartyEmoteData(chatRoot.streamer.id, true, true, true, true, cancellationToken);
+ EmoteResponse emotes = await TwitchHelper.GetThirdPartyEmotesMetadata(chatRoot.streamer.id, true, true, true, true, cancellationToken);
List itemList = new();
itemList.AddRange(emotes.BTTV);
itemList.AddRange(emotes.FFZ);
From 569e30396d7220430aef138f70eb39ec65af315b Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Wed, 13 Sep 2023 18:05:32 -0400
Subject: [PATCH 14/50] Disable nullable ref types for now
---
TwitchDownloaderCore/TwitchDownloaderCore.csproj | 2 +-
TwitchDownloaderWPF/TwitchDownloaderWPF.csproj | 10 +++++-----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/TwitchDownloaderCore/TwitchDownloaderCore.csproj b/TwitchDownloaderCore/TwitchDownloaderCore.csproj
index c0f199fe..48856a1e 100644
--- a/TwitchDownloaderCore/TwitchDownloaderCore.csproj
+++ b/TwitchDownloaderCore/TwitchDownloaderCore.csproj
@@ -10,7 +10,7 @@
AnyCPU;x64
default
true
- enable
+
diff --git a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
index 98a512a3..aba5f4b6 100644
--- a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
+++ b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
@@ -19,9 +19,9 @@
false
true
true
- false
- AnyCPU;x64
- enable
+ false
+ AnyCPU;x64
+
icon.ico
@@ -96,10 +96,10 @@
Strings.resx
-
+
Strings.resx
-
+
Strings.resx
From c0c53251355e4ff2ec35db40ea264595fb3cb183 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Wed, 13 Sep 2023 18:06:30 -0400
Subject: [PATCH 15/50] A little bit of cleanup & fix potential url list crash
---
TwitchDownloaderCore/Tools/DriveHelper.cs | 7 ++---
TwitchDownloaderWPF/Models/BooleanModel.cs | 19 ++++++------
.../Models/ResourceDictionaryModel.cs | 29 +++++++------------
TwitchDownloaderWPF/Models/SolidBrushModel.cs | 19 ++++++------
.../Services/DefaultThemeService.cs | 2 ++
.../WindowQueueOptions.xaml.cs | 3 +-
TwitchDownloaderWPF/WindowUrlList.xaml.cs | 10 +++----
7 files changed, 40 insertions(+), 49 deletions(-)
diff --git a/TwitchDownloaderCore/Tools/DriveHelper.cs b/TwitchDownloaderCore/Tools/DriveHelper.cs
index 886d2275..299cbc5a 100644
--- a/TwitchDownloaderCore/Tools/DriveHelper.cs
+++ b/TwitchDownloaderCore/Tools/DriveHelper.cs
@@ -9,13 +9,10 @@ public static class DriveHelper
{
public static DriveInfo GetOutputDrive(string outputPath)
{
- // Cannot instantiate a null DriveInfo
- DriveInfo outputDrive = DriveInfo.GetDrives()[0];
+ var outputDrive = DriveInfo.GetDrives()[0];
- // Get the name of the drive we are writing to
foreach (var drive in DriveInfo.GetDrives())
{
- // If our output path starts with the drive name
if (outputPath.StartsWith(drive.Name))
{
// In Linux, the root drive is '/' while mounted drives are located in '/mnt/' or '/run/media/'
@@ -34,7 +31,7 @@ public static DriveInfo GetOutputDrive(string outputPath)
public static async Task WaitForDrive(DriveInfo drive, IProgress progress, CancellationToken cancellationToken)
{
- int driveNotReadyCount = 0;
+ var driveNotReadyCount = 0;
while (!drive.IsReady)
{
progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Waiting for output drive ({(driveNotReadyCount + 1) / 2f:F1}s)"));
diff --git a/TwitchDownloaderWPF/Models/BooleanModel.cs b/TwitchDownloaderWPF/Models/BooleanModel.cs
index 807708c2..79f98741 100644
--- a/TwitchDownloaderWPF/Models/BooleanModel.cs
+++ b/TwitchDownloaderWPF/Models/BooleanModel.cs
@@ -2,12 +2,13 @@
namespace TwitchDownloaderWPF.Models
{
- [XmlRoot(ElementName = "Boolean", Namespace = "clr-namespace:System;assembly=mscorlib")]
- public class BooleanModel
- {
- [XmlAttribute(AttributeName = "Key", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml")]
- public string Key { get; set; }
- [XmlText(Type = typeof(bool))]
- public bool Value { get; set; }
- }
-}
+ [XmlRoot(ElementName = "Boolean", Namespace = "clr-namespace:System;assembly=mscorlib")]
+ public class BooleanModel
+ {
+ [XmlAttribute(AttributeName = "Key", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml")]
+ public string Key { get; set; }
+
+ [XmlText(Type = typeof(bool))]
+ public bool Value { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Models/ResourceDictionaryModel.cs b/TwitchDownloaderWPF/Models/ResourceDictionaryModel.cs
index c4a93fd5..7315dd2c 100644
--- a/TwitchDownloaderWPF/Models/ResourceDictionaryModel.cs
+++ b/TwitchDownloaderWPF/Models/ResourceDictionaryModel.cs
@@ -3,22 +3,13 @@
namespace TwitchDownloaderWPF.Models
{
- [XmlRoot(ElementName = "ResourceDictionary", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation")]
- public class ResourceDictionaryModel
- {
- [XmlElement(ElementName = "SolidColorBrush", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation")]
- public List SolidColorBrush { get; set; }
-
- [XmlElement(ElementName = "Boolean", Namespace = "clr-namespace:System;assembly=mscorlib")]
- public List Boolean { get; set; }
-
- [XmlAttribute(AttributeName = "xmlns")]
- public string Xmlns { get; set; }
-
- [XmlAttribute(AttributeName = "x", Namespace = "http://www.w3.org/2000/xmlns/")]
- public string X { get; set; }
-
- [XmlAttribute(AttributeName ="system", Namespace = "clr-namespace:System;assembly=mscorlib")]
- public string System { get; set; }
- }
-}
+ [XmlRoot(ElementName = "ResourceDictionary", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation")]
+ public class ResourceDictionaryModel
+ {
+ [XmlElement(ElementName = "SolidColorBrush", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation")]
+ public List SolidColorBrush { get; set; }
+
+ [XmlElement(ElementName = "Boolean", Namespace = "clr-namespace:System;assembly=mscorlib")]
+ public List Boolean { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Models/SolidBrushModel.cs b/TwitchDownloaderWPF/Models/SolidBrushModel.cs
index 90fd8ab9..39a79eca 100644
--- a/TwitchDownloaderWPF/Models/SolidBrushModel.cs
+++ b/TwitchDownloaderWPF/Models/SolidBrushModel.cs
@@ -2,12 +2,13 @@
namespace TwitchDownloaderWPF.Models
{
- [XmlRoot(ElementName = "SolidColorBrush", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation")]
- public class SolidColorBrushModel
- {
- [XmlAttribute(AttributeName = "Key", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml")]
- public string Key { get; set; }
- [XmlAttribute(AttributeName = "Color")]
- public string Color { get; set; }
- }
-}
+ [XmlRoot(ElementName = "SolidColorBrush", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation")]
+ public class SolidColorBrushModel
+ {
+ [XmlAttribute(AttributeName = "Key", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml")]
+ public string Key { get; set; }
+
+ [XmlAttribute(AttributeName = "Color")]
+ public string Color { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Services/DefaultThemeService.cs b/TwitchDownloaderWPF/Services/DefaultThemeService.cs
index 1666ddd6..325b3c84 100644
--- a/TwitchDownloaderWPF/Services/DefaultThemeService.cs
+++ b/TwitchDownloaderWPF/Services/DefaultThemeService.cs
@@ -15,6 +15,8 @@ public static bool WriteIncludedThemes()
foreach (var themeResourcePath in themeResourcePaths)
{
using var themeStream = GetResourceStream(themeResourcePath);
+ if (themeStream is null) continue;
+
var themePathSplit = themeResourcePath.Split(".");
var themeName = themePathSplit[^2];
diff --git a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
index 1ff65b10..fb0b4785 100644
--- a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
+++ b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
@@ -37,7 +37,7 @@ public WindowQueueOptions(Page page)
checkVideo.IsChecked = true;
checkVideo.IsEnabled = false;
}
- if (page is PageChatDownload)
+ if (page is PageChatDownload chatPage)
{
checkVideo.Visibility = Visibility.Collapsed;
checkChat.IsChecked = true;
@@ -50,7 +50,6 @@ public WindowQueueOptions(Page page)
RadioCompressionNone.Visibility = Visibility.Collapsed;
RadioCompressionGzip.Visibility = Visibility.Collapsed;
checkEmbed.Visibility = Visibility.Collapsed;
- var chatPage = page as PageChatDownload;
if (chatPage.radioJson.IsChecked != true)
{
checkRender.IsChecked = false;
diff --git a/TwitchDownloaderWPF/WindowUrlList.xaml.cs b/TwitchDownloaderWPF/WindowUrlList.xaml.cs
index 08e5e914..44821370 100644
--- a/TwitchDownloaderWPF/WindowUrlList.xaml.cs
+++ b/TwitchDownloaderWPF/WindowUrlList.xaml.cs
@@ -35,7 +35,7 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e)
{
string id = PageChatDownload.ValidateUrl(url);
- if (id == "")
+ if (string.IsNullOrWhiteSpace(id))
{
invalidList.Add(url);
}
@@ -162,9 +162,9 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e)
}
private void Window_Loaded(object sender, RoutedEventArgs e)
- {
+ {
Title = Translations.Strings.TitleUrlList;
- App.RequestTitleBarChange();
- }
+ App.RequestTitleBarChange();
+ }
}
-}
+}
\ No newline at end of file
From d8786b2074f0c581f32bfffe42f19306701d31f8 Mon Sep 17 00:00:00 2001
From: Matthew Davis <45373823+davis-matthew@users.noreply.github.com>
Date: Fri, 15 Sep 2023 00:03:44 -0400
Subject: [PATCH 16/50] Spelling fixes (#814)
---
TwitchDownloaderCLI/Modes/UpdateChat.cs | 2 +-
TwitchDownloaderCore/ChatUpdater.cs | 4 ++--
TwitchDownloaderCore/TwitchObjects/StvEmoteFlags.cs | 2 +-
TwitchDownloaderWPF/README.md | 2 +-
TwitchDownloaderWPF/WindowMassDownload.xaml.cs | 2 +-
TwitchDownloaderWPF/WindowQueueOptions.xaml.cs | 2 +-
6 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/TwitchDownloaderCLI/Modes/UpdateChat.cs b/TwitchDownloaderCLI/Modes/UpdateChat.cs
index a4a4e213..bc214f9c 100644
--- a/TwitchDownloaderCLI/Modes/UpdateChat.cs
+++ b/TwitchDownloaderCLI/Modes/UpdateChat.cs
@@ -54,7 +54,7 @@ private static ChatUpdateOptions GetUpdateOptions(ChatUpdateArgs inputOptions)
if (Path.GetFullPath(inputOptions.InputFile!) == Path.GetFullPath(inputOptions.OutputFile!))
{
- Console.WriteLine("[WARNING] - Output file path is identical to input file. This is not recommended in case something goes wrong. All data will be permanantly overwritten!");
+ Console.WriteLine("[WARNING] - Output file path is identical to input file. This is not recommended in case something goes wrong. All data will be permanently overwritten!");
}
ChatUpdateOptions updateOptions = new()
diff --git a/TwitchDownloaderCore/ChatUpdater.cs b/TwitchDownloaderCore/ChatUpdater.cs
index 9ac3c06a..2fb1b315 100644
--- a/TwitchDownloaderCore/ChatUpdater.cs
+++ b/TwitchDownloaderCore/ChatUpdater.cs
@@ -271,7 +271,7 @@ private async Task ChatBeginningCropTask(IProgress progress, Can
}
// Adjust the crop parameter
- double beginningCropClamp = double.IsNegative(chatRoot.video.length) ? 172_800 : chatRoot.video.length; // Get length from chatroot or if negavite (N/A) max vod length (48 hours) in seconds. https://help.twitch.tv/s/article/broadcast-guidelines
+ double beginningCropClamp = double.IsNegative(chatRoot.video.length) ? 172_800 : chatRoot.video.length; // Get length from chatroot or if negative (N/A) max vod length (48 hours) in seconds. https://help.twitch.tv/s/article/broadcast-guidelines
chatRoot.video.start = Math.Min(Math.Max(_updateOptions.CropBeginningTime, 0.0), beginningCropClamp);
}
@@ -304,7 +304,7 @@ private async Task ChatEndingCropTask(IProgress progress, Cancel
}
// Adjust the crop parameter
- double endingCropClamp = double.IsNegative(chatRoot.video.length) ? 172_800 : chatRoot.video.length; // Get length from chatroot or if negavite (N/A) max vod length (48 hours) in seconds. https://help.twitch.tv/s/article/broadcast-guidelines
+ double endingCropClamp = double.IsNegative(chatRoot.video.length) ? 172_800 : chatRoot.video.length; // Get length from chatroot or if negative (N/A) max vod length (48 hours) in seconds. https://help.twitch.tv/s/article/broadcast-guidelines
chatRoot.video.end = Math.Min(Math.Max(_updateOptions.CropEndingTime, 0.0), endingCropClamp);
}
diff --git a/TwitchDownloaderCore/TwitchObjects/StvEmoteFlags.cs b/TwitchDownloaderCore/TwitchObjects/StvEmoteFlags.cs
index fb5c8741..e91653b0 100644
--- a/TwitchDownloaderCore/TwitchObjects/StvEmoteFlags.cs
+++ b/TwitchDownloaderCore/TwitchObjects/StvEmoteFlags.cs
@@ -13,7 +13,7 @@ public enum StvEmoteFlags
// Content Flags
- ContentSexual = 1 << 16, // Sexually Suggesive
+ ContentSexual = 1 << 16, // Sexually Suggestive
ContentEpilepsy = 1 << 17, // Rapid flashing
ContentEdgy = 1 << 18, // Edgy or distasteful, may be offensive to some users
ContentTwitchDisallowed = 1 << 24, // Not allowed specifically on the Twitch platform
diff --git a/TwitchDownloaderWPF/README.md b/TwitchDownloaderWPF/README.md
index d88e24a7..bedbb143 100644
--- a/TwitchDownloaderWPF/README.md
+++ b/TwitchDownloaderWPF/README.md
@@ -53,7 +53,7 @@ Downloads a clip from Twitch.
![Figure 2.1](Images/clipExample.png)
*Figure 2.1*
-To get started, input a valid link or ID to a clip. From there the the download options will unlock, allowing you to customize the job.
+To get started, input a valid link or ID to a clip. From there the download options will unlock, allowing you to customize the job.
**Quality**: Selects the quality of the clip before downloading.
diff --git a/TwitchDownloaderWPF/WindowMassDownload.xaml.cs b/TwitchDownloaderWPF/WindowMassDownload.xaml.cs
index 37dbe3e4..c74b092a 100644
--- a/TwitchDownloaderWPF/WindowMassDownload.xaml.cs
+++ b/TwitchDownloaderWPF/WindowMassDownload.xaml.cs
@@ -224,7 +224,7 @@ private async void comboSort_SelectionChanged(object sender, SelectionChangedEve
private void btnSelectAll_Click(object sender, RoutedEventArgs e)
{
- //I'm sure there is a much better way to do this. Could not find a way to itterate over each itemcontrol border
+ //I'm sure there is a much better way to do this. Could not find a way to iterate over each itemcontrol border
foreach (var video in videoList)
{
if (!selectedItems.Any(x => x.Id == video.Id))
diff --git a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
index fb0b4785..5f757712 100644
--- a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
+++ b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
@@ -18,7 +18,7 @@ namespace TwitchDownloaderWPF
///
public partial class WindowQueueOptions : Window
{
- // This file is absolutely attrocious, but fixing it would mean rewriting the entire GUI in a more abstract form
+ // This file is absolutely atrocious, but fixing it would mean rewriting the entire GUI in a more abstract form
List dataList;
From c1ccdfe8f7f4fbadf763d1dfd63081861f3f49d9 Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Sat, 16 Sep 2023 02:27:27 -0400
Subject: [PATCH 17/50] Add support for dark title bars on Windows 10 1809-2004
(#815)
* Support dark title bar on Windows 10 versions between 1809 and 2004 (20H1)
* Decided I prefer having it in this file
---
.../Services/NativeFunctions.cs | 2 +-
TwitchDownloaderWPF/Services/ThemeService.cs | 24 ++++++++++++-------
.../Services/WindowsThemeService.cs | 4 +---
3 files changed, 17 insertions(+), 13 deletions(-)
diff --git a/TwitchDownloaderWPF/Services/NativeFunctions.cs b/TwitchDownloaderWPF/Services/NativeFunctions.cs
index 4a33f7dc..332fe2a5 100644
--- a/TwitchDownloaderWPF/Services/NativeFunctions.cs
+++ b/TwitchDownloaderWPF/Services/NativeFunctions.cs
@@ -8,6 +8,6 @@ namespace TwitchDownloaderWPF.Services
public static class NativeFunctions
{
[DllImport("dwmapi.dll", EntryPoint = "DwmSetWindowAttribute", PreserveSig = true)]
- public static extern int SetWindowAttribute(IntPtr handle, int attribute, ref bool attributeValue, int attributeSize);
+ public static extern int SetWindowAttribute(IntPtr handle, int attribute, [In] ref int attributeValue, int attributeSize);
}
}
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Services/ThemeService.cs b/TwitchDownloaderWPF/Services/ThemeService.cs
index 8c633ca2..477f3a2e 100644
--- a/TwitchDownloaderWPF/Services/ThemeService.cs
+++ b/TwitchDownloaderWPF/Services/ThemeService.cs
@@ -1,9 +1,9 @@
using HandyControl.Data;
using System;
using System.IO;
-using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Windows;
+using System.Windows.Interop;
using System.Windows.Media;
using System.Xml.Serialization;
using TwitchDownloaderWPF.Models;
@@ -13,7 +13,10 @@ namespace TwitchDownloaderWPF.Services
{
public class ThemeService
{
- private const int TITLEBAR_THEME_ATTRIBUTE = 20;
+ private const int WINDOWS_1809_BUILD_NUMBER = 17763;
+ private const int WINDOWS_2004_INSIDER_BUILD_NUMBER = 18985;
+ private const int USE_IMMERSIVE_DARK_MODE_ATTRIBUTE_BEFORE_2004 = 19;
+ private const int USE_IMMERSIVE_DARK_MODE_ATTRIBUTE = 20;
private bool _darkAppTitleBar = false;
private bool _darkHandyControl = false;
@@ -84,16 +87,18 @@ public void ChangeAppTheme()
[SupportedOSPlatform("windows")]
public void SetTitleBarTheme(WindowCollection windows)
{
- // If windows 10 build is before 1903, it doesn't support dark title bars
- if (Environment.OSVersion.Version.Build < 18362)
- {
+ if (Environment.OSVersion.Version.Major < 10 || Environment.OSVersion.Version.Build < WINDOWS_1809_BUILD_NUMBER)
return;
- }
+
+ var shouldUseDarkTitleBar = Convert.ToInt32(_darkAppTitleBar);
+ var darkTitleBarAttribute = Environment.OSVersion.Version.Build < WINDOWS_2004_INSIDER_BUILD_NUMBER
+ ? USE_IMMERSIVE_DARK_MODE_ATTRIBUTE_BEFORE_2004
+ : USE_IMMERSIVE_DARK_MODE_ATTRIBUTE;
foreach (Window window in windows)
{
- var windowHandle = new System.Windows.Interop.WindowInteropHelper(window).Handle;
- NativeFunctions.SetWindowAttribute(windowHandle, TITLEBAR_THEME_ATTRIBUTE, ref _darkAppTitleBar, Marshal.SizeOf(_darkAppTitleBar));
+ var windowHandle = new WindowInteropHelper(window).Handle;
+ NativeFunctions.SetWindowAttribute(windowHandle, darkTitleBarAttribute, ref shouldUseDarkTitleBar, sizeof(int));
}
Window wnd = new()
@@ -104,7 +109,8 @@ public void SetTitleBarTheme(WindowCollection windows)
};
wnd.Show();
wnd.Close();
- // Dark title bar is a bit buggy, requires window resize or focus change to fully apply
+ // Dark title bar is a bit buggy, requires window redraw (focus change, resize, transparency change) to fully apply.
+ // We *could* send a repaint message to win32.dll, but this solution works and is way easier.
// Win11 might not have this issue but Win10 does so please leave this
}
diff --git a/TwitchDownloaderWPF/Services/WindowsThemeService.cs b/TwitchDownloaderWPF/Services/WindowsThemeService.cs
index 04bea753..aebcbd54 100644
--- a/TwitchDownloaderWPF/Services/WindowsThemeService.cs
+++ b/TwitchDownloaderWPF/Services/WindowsThemeService.cs
@@ -17,15 +17,13 @@ public class WindowsThemeService : ManagementEventWatcher
private const string REGISTRY_KEY_NAME = "AppsUseLightTheme";
private const string LIGHT_THEME = "Light";
private const string DARK_THEME = "Dark";
+ private const int WINDOWS_1809_BUILD_NUMBER = 17763;
public WindowsThemeService()
{
// If the OS is older than Windows 10 1809 then it doesn't have the app theme registry key
- const int WINDOWS_1809_BUILD_NUMBER = 17763;
if (Environment.OSVersion.Version.Major < 10 || Environment.OSVersion.Version.Build < WINDOWS_1809_BUILD_NUMBER)
- {
return;
- }
var currentUser = WindowsIdentity.GetCurrent().User;
From a9ed1cf4a838f114263652faeb8b88ee99b916af Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Mon, 18 Sep 2023 22:49:13 -0400
Subject: [PATCH 18/50] Fix incorrect message.bits_spent for chats between v5
shutdown and 1.51.2 (#817)
---
TwitchDownloaderCore/Chat/ChatJson.cs | 14 ++++++++++++++
TwitchDownloaderCore/ChatDownloader.cs | 6 +-----
TwitchDownloaderCore/Tools/TwitchRegex.cs | 3 +++
3 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/TwitchDownloaderCore/Chat/ChatJson.cs b/TwitchDownloaderCore/Chat/ChatJson.cs
index d301921e..1eeb3f4f 100644
--- a/TwitchDownloaderCore/Chat/ChatJson.cs
+++ b/TwitchDownloaderCore/Chat/ChatJson.cs
@@ -8,6 +8,7 @@
using System.Threading;
using System.Threading.Tasks;
using TwitchDownloaderCore.Extensions;
+using TwitchDownloaderCore.Tools;
using TwitchDownloaderCore.TwitchObjects;
namespace TwitchDownloaderCore.Chat
@@ -145,6 +146,19 @@ private static async ValueTask UpgradeChatJson(ChatRoot chatRoot)
chatRoot.video.end = chatRoot.video.length;
chatRoot.video.duration = null;
}
+
+ // Fix incorrect bits_spent value on chats between v5 shutdown and the lay295#520 fix
+ if (chatRoot.comments.All(c => c.message.bits_spent == 0))
+ {
+ foreach (var comment in chatRoot.comments)
+ {
+ var bitMatch = TwitchRegex.BitsRegex.Match(comment.message.body);
+ if (bitMatch.Success && int.TryParse(bitMatch.ValueSpan, out var result))
+ {
+ comment.message.bits_spent = result;
+ }
+ }
+ }
}
///
diff --git a/TwitchDownloaderCore/ChatDownloader.cs b/TwitchDownloaderCore/ChatDownloader.cs
index e4f2a0dc..a0932324 100644
--- a/TwitchDownloaderCore/ChatDownloader.cs
+++ b/TwitchDownloaderCore/ChatDownloader.cs
@@ -5,7 +5,6 @@
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using TwitchDownloaderCore.Chat;
@@ -25,9 +24,6 @@ public sealed class ChatDownloader
BaseAddress = new Uri("https://gql.twitch.tv/gql"),
DefaultRequestHeaders = { { "Client-ID", "kd1unb4b3q4t58fwlpcbzcbnm76a8fp" } }
};
- private static readonly Regex BitsRegex = new(
- @"(?<=(?:\s|^)(?:4Head|Anon|Bi(?:bleThumb|tBoss)|bday|C(?:h(?:eer|arity)|orgo)|cheerwal|D(?:ansGame|oodleCheer)|EleGiggle|F(?:rankerZ|ailFish)|Goal|H(?:eyGuys|olidayCheer)|K(?:appa|reygasm)|M(?:rDestructoid|uxy)|NotLikeThis|P(?:arty|ride|JSalt)|RIPCheer|S(?:coops|h(?:owLove|amrock)|eemsGood|wiftRage|treamlabs)|TriHard|uni|VoHiYo))[1-9]\d?\d?\d?\d?\d?\d?(?=\s|$)",
- RegexOptions.Compiled);
private enum DownloadType
{
@@ -231,7 +227,7 @@ private static List ConvertComments(CommentVideo video, ChatFormat form
message.body = bodyStringBuilder.ToString();
- var bitMatch = BitsRegex.Match(message.body);
+ var bitMatch = TwitchRegex.BitsRegex.Match(message.body);
if (bitMatch.Success && int.TryParse(bitMatch.ValueSpan, out var result))
{
message.bits_spent = result;
diff --git a/TwitchDownloaderCore/Tools/TwitchRegex.cs b/TwitchDownloaderCore/Tools/TwitchRegex.cs
index 8905a2e2..4005d5b5 100644
--- a/TwitchDownloaderCore/Tools/TwitchRegex.cs
+++ b/TwitchDownloaderCore/Tools/TwitchRegex.cs
@@ -11,6 +11,9 @@ public static class TwitchRegex
private static readonly Regex ClipId = new(@"(?<=^|(?:clips\.)?twitch\.tv\/(?:\w+\/clip\/)?)[\w-]+?(?=$|\?|\s)", RegexOptions.Compiled);
public static readonly Regex UrlTimeCode = new(@"(?<=(?:\?|&)t=)\d+h\d+m\d+s(?=$|\?|\s)", RegexOptions.Compiled);
+ public static readonly Regex BitsRegex = new(
+ @"(?<=(?:\s|^)(?:4Head|Anon|Bi(?:bleThumb|tBoss)|bday|C(?:h(?:eer|arity)|orgo)|cheerwal|D(?:ansGame|oodleCheer)|EleGiggle|F(?:rankerZ|ailFish)|Goal|H(?:eyGuys|olidayCheer)|K(?:appa|reygasm)|M(?:rDestructoid|uxy)|NotLikeThis|P(?:arty|ride|JSalt)|RIPCheer|S(?:coops|h(?:owLove|amrock)|eemsGood|wiftRage|treamlabs)|TriHard|uni|VoHiYo))[1-9]\d?\d?\d?\d?\d?\d?(?=\s|$)",
+ RegexOptions.Compiled);
/// A of the video's id or .
public static Match MatchVideoId(string text)
From 29a2c44aca7b6944fa1c928fb537788f8fa8cf77 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Tue, 19 Sep 2023 01:14:33 -0400
Subject: [PATCH 19/50] Cleanup
---
.../Modes/Arguments/CacheArgs.cs | 1 -
.../Modes/Arguments/ChatDownloadArgs.cs | 7 +++----
.../Modes/Arguments/ChatRenderArgs.cs | 1 -
.../Modes/Arguments/ChatUpdateArgs.cs | 1 -
.../Modes/Arguments/ClipDownloadArgs.cs | 1 -
.../Modes/Arguments/FfmpegArgs.cs | 1 -
.../Modes/Arguments/VideoDownloadArgs.cs | 1 -
TwitchDownloaderCLI/Tools/FfmpegHandler.cs | 20 +++++++++----------
TwitchDownloaderCore/ChatRenderer.cs | 7 +++----
TwitchDownloaderCore/ClipDownloader.cs | 13 ++++++------
.../Options/ChatRenderOptions.cs | 4 ++--
TwitchDownloaderCore/Tools/FfmpegProcess.cs | 2 --
.../Tools/VideoSizeEstimator.cs | 1 -
TwitchDownloaderCore/TwitchHelper.cs | 19 ++++++++----------
.../Gql/GqlClipSearchResponse.cs | 2 --
TwitchDownloaderWPF/PageVodDownload.xaml.cs | 14 ++++++-------
TwitchDownloaderWPF/WindowMassDownload.xaml | 4 ++--
17 files changed, 42 insertions(+), 57 deletions(-)
diff --git a/TwitchDownloaderCLI/Modes/Arguments/CacheArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/CacheArgs.cs
index 2b30d6fe..95d1ea76 100644
--- a/TwitchDownloaderCLI/Modes/Arguments/CacheArgs.cs
+++ b/TwitchDownloaderCLI/Modes/Arguments/CacheArgs.cs
@@ -2,7 +2,6 @@
namespace TwitchDownloaderCLI.Modes.Arguments
{
-
[Verb("cache", HelpText = "Manage the working cache")]
public class CacheArgs : ITwitchDownloaderArgs
{
diff --git a/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs
index 66add8ca..6e4d5a10 100644
--- a/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs
+++ b/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs
@@ -3,7 +3,6 @@
namespace TwitchDownloaderCLI.Modes.Arguments
{
-
[Verb("chatdownload", HelpText = "Downloads the chat from a VOD or clip")]
public class ChatDownloadArgs : ITwitchDownloaderArgs
{
@@ -21,16 +20,16 @@ public class ChatDownloadArgs : ITwitchDownloaderArgs
[Option('e', "ending", HelpText = "Time in seconds to crop ending.")]
public double CropEndingTime { get; set; }
-
+
[Option('E', "embed-images", Default = false, HelpText = "Embed first party emotes, badges, and cheermotes into the chat download for offline rendering.")]
public bool EmbedData { get; set; }
[Option("bttv", Default = true, HelpText = "Enable BTTV embedding in chat download. Requires -E / --embed-images!")]
public bool? BttvEmotes { get; set; }
-
+
[Option("ffz", Default = true, HelpText = "Enable FFZ embedding in chat download. Requires -E / --embed-images!")]
public bool? FfzEmotes { get; set; }
-
+
[Option("stv", Default = true, HelpText = "Enable 7TV embedding in chat download. Requires -E / --embed-images!")]
public bool? StvEmotes { get; set; }
diff --git a/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs
index 6ba9254e..b49a1e7d 100644
--- a/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs
+++ b/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs
@@ -2,7 +2,6 @@
namespace TwitchDownloaderCLI.Modes.Arguments
{
-
[Verb("chatrender", HelpText = "Renders a chat JSON as a video")]
public class ChatRenderArgs : ITwitchDownloaderArgs
{
diff --git a/TwitchDownloaderCLI/Modes/Arguments/ChatUpdateArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/ChatUpdateArgs.cs
index bec5865e..0ebd86f8 100644
--- a/TwitchDownloaderCLI/Modes/Arguments/ChatUpdateArgs.cs
+++ b/TwitchDownloaderCLI/Modes/Arguments/ChatUpdateArgs.cs
@@ -3,7 +3,6 @@
namespace TwitchDownloaderCLI.Modes.Arguments
{
-
[Verb("chatupdate", HelpText = "Updates the embedded emotes, badges, bits, and crops of a chat download and/or converts a JSON chat to another format.")]
public class ChatUpdateArgs : ITwitchDownloaderArgs
{
diff --git a/TwitchDownloaderCLI/Modes/Arguments/ClipDownloadArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/ClipDownloadArgs.cs
index ec06dd27..bb88c7df 100644
--- a/TwitchDownloaderCLI/Modes/Arguments/ClipDownloadArgs.cs
+++ b/TwitchDownloaderCLI/Modes/Arguments/ClipDownloadArgs.cs
@@ -2,7 +2,6 @@
namespace TwitchDownloaderCLI.Modes.Arguments
{
-
[Verb("clipdownload", HelpText = "Downloads a clip from Twitch")]
public class ClipDownloadArgs : ITwitchDownloaderArgs
{
diff --git a/TwitchDownloaderCLI/Modes/Arguments/FfmpegArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/FfmpegArgs.cs
index 1979b8c9..8abe5d64 100644
--- a/TwitchDownloaderCLI/Modes/Arguments/FfmpegArgs.cs
+++ b/TwitchDownloaderCLI/Modes/Arguments/FfmpegArgs.cs
@@ -2,7 +2,6 @@
namespace TwitchDownloaderCLI.Modes.Arguments
{
-
[Verb("ffmpeg", HelpText = "Manage standalone ffmpeg")]
public class FfmpegArgs : ITwitchDownloaderArgs
{
diff --git a/TwitchDownloaderCLI/Modes/Arguments/VideoDownloadArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/VideoDownloadArgs.cs
index fe4e09fb..33de037e 100644
--- a/TwitchDownloaderCLI/Modes/Arguments/VideoDownloadArgs.cs
+++ b/TwitchDownloaderCLI/Modes/Arguments/VideoDownloadArgs.cs
@@ -2,7 +2,6 @@
namespace TwitchDownloaderCLI.Modes.Arguments
{
-
[Verb("videodownload", HelpText = "Downloads a stream VOD from Twitch")]
public class VideoDownloadArgs : ITwitchDownloaderArgs
{
diff --git a/TwitchDownloaderCLI/Tools/FfmpegHandler.cs b/TwitchDownloaderCLI/Tools/FfmpegHandler.cs
index 5be21fa3..8684d8e6 100644
--- a/TwitchDownloaderCLI/Tools/FfmpegHandler.cs
+++ b/TwitchDownloaderCLI/Tools/FfmpegHandler.cs
@@ -62,20 +62,20 @@ public static void DetectFfmpeg(string ffmpegPath)
Console.WriteLine("[ERROR] - Unable to find FFmpeg, exiting. You can download FFmpeg automatically with the command \"TwitchDownloaderCLI ffmpeg -d\"");
Environment.Exit(1);
}
- }
-
- internal class XabeProgressHandler
- {
- private int _lastPercent = -1;
- internal void OnProgressReceived(object sender, ProgressInfo e)
+ private class XabeProgressHandler
{
- var percent = (int)(e.DownloadedBytes / (double)e.TotalBytes * 100);
+ private int _lastPercent = -1;
- if (percent > _lastPercent)
+ internal void OnProgressReceived(object sender, ProgressInfo e)
{
- _lastPercent = percent;
- Console.Write($"\r[INFO] - Downloading FFmpeg {percent}%");
+ var percent = (int)(e.DownloadedBytes / (double)e.TotalBytes * 100);
+
+ if (percent > _lastPercent)
+ {
+ _lastPercent = percent;
+ Console.Write($"\r[INFO] - Downloading FFmpeg {percent}%");
+ }
}
}
}
diff --git a/TwitchDownloaderCore/ChatRenderer.cs b/TwitchDownloaderCore/ChatRenderer.cs
index adca1ed5..78fc1265 100644
--- a/TwitchDownloaderCore/ChatRenderer.cs
+++ b/TwitchDownloaderCore/ChatRenderer.cs
@@ -387,7 +387,7 @@ private FfmpegProcess GetFfmpegProcess(int partNumber, bool isMask)
if (renderOptions.LogFfmpegOutput && _progress != null)
{
- process.ErrorDataReceived += (s, e) =>
+ process.ErrorDataReceived += (_, e) =>
{
if (e.Data != null)
{
@@ -644,7 +644,6 @@ private SKBitmap CombineImages(List<(SKImageInfo info, SKBitmap bitmap)> section
finalCanvas.DrawBitmap(sectionImages[i].bitmap, 0, i * renderOptions.SectionHeight);
sectionImages[i].bitmap.Dispose();
}
-
}
sectionImages.Clear();
finalBitmap.SetImmutable();
@@ -1306,9 +1305,9 @@ private void DrawUsername(Comment comment, List<(SKImageInfo info, SKBitmap bitm
DrawText(userName, userPaint, true, sectionImages, ref drawPos, defaultPos, false);
}
- private static SKColor GenerateUserColor(SKColor userColor, SKColor background_color, ChatRenderOptions renderOptions)
+ private static SKColor GenerateUserColor(SKColor userColor, SKColor backgroundColor, ChatRenderOptions renderOptions)
{
- background_color.ToHsl(out _, out _, out float backgroundBrightness);
+ backgroundColor.ToHsl(out _, out _, out float backgroundBrightness);
userColor.ToHsl(out float userHue, out float userSaturation, out float userBrightness);
if (backgroundBrightness < 25 || renderOptions.Outline)
diff --git a/TwitchDownloaderCore/ClipDownloader.cs b/TwitchDownloaderCore/ClipDownloader.cs
index bfcbe3e5..75884d87 100644
--- a/TwitchDownloaderCore/ClipDownloader.cs
+++ b/TwitchDownloaderCore/ClipDownloader.cs
@@ -9,6 +9,7 @@
using TwitchDownloaderCore.Extensions;
using TwitchDownloaderCore.Options;
using TwitchDownloaderCore.Tools;
+using TwitchDownloaderCore.TwitchObjects.Gql;
namespace TwitchDownloaderCore
{
@@ -32,6 +33,7 @@ public async Task DownloadAsync(CancellationToken cancellationToken)
_progress.Report(new ProgressReport(ReportType.NewLineStatus, "Fetching Clip Info"));
var downloadUrl = await GetDownloadUrl();
+ var clipInfo = await TwitchHelper.GetClipInfo(downloadOptions.Id);
cancellationToken.ThrowIfCancellationRequested();
@@ -69,7 +71,7 @@ void DownloadProgressHandler(StreamCopyProgress streamProgress)
_progress.Report(new ProgressReport(ReportType.NewLineStatus, "Encoding Clip Metadata 0%"));
_progress.Report(new ProgressReport(0));
- await EncodeClipMetadata(tempFile, downloadOptions.Filename, cancellationToken);
+ await EncodeClipWithMetadata(tempFile, downloadOptions.Filename, clipInfo.data.clip, cancellationToken);
_progress.Report(new ProgressReport(ReportType.SameLineStatus, "Encoding Clip Metadata 100%"));
_progress.Report(new ProgressReport(100));
@@ -135,15 +137,14 @@ private static async Task DownloadFileTaskAsync(string url, string destinationFi
}
}
- private async Task EncodeClipMetadata(string inputFile, string destinationFile, CancellationToken cancellationToken)
+ private async Task EncodeClipWithMetadata(string inputFile, string destinationFile, Clip clipMetadata, CancellationToken cancellationToken)
{
- var metadataFile = $"{Path.GetFileNameWithoutExtension(inputFile)}_metadata{Path.GetExtension(inputFile)}";
- var clipInfo = await TwitchHelper.GetClipInfo(downloadOptions.Id);
+ var metadataFile = $"{Path.GetFileName(inputFile)}_metadata.txt";
try
{
- await FfmpegMetadata.SerializeAsync(metadataFile, clipInfo.data.clip.broadcaster.displayName, downloadOptions.Id, clipInfo.data.clip.title, clipInfo.data.clip.createdAt,
- clipInfo.data.clip.viewCount, cancellationToken: cancellationToken);
+ await FfmpegMetadata.SerializeAsync(metadataFile, clipMetadata.broadcaster.displayName, downloadOptions.Id, clipMetadata.title, clipMetadata.createdAt, clipMetadata.viewCount,
+ cancellationToken: cancellationToken);
var process = new Process
{
diff --git a/TwitchDownloaderCore/Options/ChatRenderOptions.cs b/TwitchDownloaderCore/Options/ChatRenderOptions.cs
index 80a42ba1..b7d061f0 100644
--- a/TwitchDownloaderCore/Options/ChatRenderOptions.cs
+++ b/TwitchDownloaderCore/Options/ChatRenderOptions.cs
@@ -49,8 +49,8 @@ public string MaskFile
if (OutputFile == "" || GenerateMask == false)
return OutputFile;
- string extension = Path.GetExtension(OutputFile);
- int extensionIndex = OutputFile.LastIndexOf(extension);
+ string extension = Path.GetExtension(OutputFile)!;
+ int extensionIndex = OutputFile!.LastIndexOf(extension, StringComparison.Ordinal);
return string.Concat(OutputFile.AsSpan(0, extensionIndex), "_mask", extension);
}
}
diff --git a/TwitchDownloaderCore/Tools/FfmpegProcess.cs b/TwitchDownloaderCore/Tools/FfmpegProcess.cs
index c81a1bc0..668a9749 100644
--- a/TwitchDownloaderCore/Tools/FfmpegProcess.cs
+++ b/TwitchDownloaderCore/Tools/FfmpegProcess.cs
@@ -5,7 +5,5 @@ namespace TwitchDownloaderCore.Tools
public sealed class FfmpegProcess : Process
{
public string SavePath { get; init; }
-
- public FfmpegProcess() { }
}
}
\ No newline at end of file
diff --git a/TwitchDownloaderCore/Tools/VideoSizeEstimator.cs b/TwitchDownloaderCore/Tools/VideoSizeEstimator.cs
index fa8d8890..eec8badb 100644
--- a/TwitchDownloaderCore/Tools/VideoSizeEstimator.cs
+++ b/TwitchDownloaderCore/Tools/VideoSizeEstimator.cs
@@ -31,6 +31,5 @@ public static long EstimateVideoSize(int bandwidth, TimeSpan startTime, TimeSpan
var totalTime = endTime - startTime;
return (long)(bandwidth / 8d * totalTime.TotalSeconds);
}
-
}
}
\ No newline at end of file
diff --git a/TwitchDownloaderCore/TwitchHelper.cs b/TwitchDownloaderCore/TwitchHelper.cs
index fdbe006a..81918791 100644
--- a/TwitchDownloaderCore/TwitchHelper.cs
+++ b/TwitchDownloaderCore/TwitchHelper.cs
@@ -1,5 +1,4 @@
-using NeoSmart.Unicode;
-using SkiaSharp;
+using SkiaSharp;
using System;
using System.Collections.Generic;
using System.IO;
@@ -23,7 +22,7 @@ namespace TwitchDownloaderCore
public static class TwitchHelper
{
private static readonly HttpClient httpClient = new HttpClient();
- private static readonly string[] bttvZeroWidth = { "SoSnowy", "IceCold", "SantaHat", "TopHat", "ReinDeer", "CandyCane", "cvMask", "cvHazmat" };
+ private static readonly string[] BttvZeroWidth = { "SoSnowy", "IceCold", "SantaHat", "TopHat", "ReinDeer", "CandyCane", "cvMask", "cvHazmat" };
public static async Task GetVideoInfo(int videoId)
{
@@ -178,7 +177,7 @@ private static async Task> GetBttvEmotesMetadata(int str
string name = emote.code;
string mime = emote.imageType;
string url = $"https://cdn.betterttv.net/emote/{id}/[scale]x";
- returnList.Add(new EmoteResponseItem() { Id = id, Code = name, ImageType = mime, ImageUrl = url, IsZeroWidth = bttvZeroWidth.Contains(name) });
+ returnList.Add(new EmoteResponseItem() { Id = id, Code = name, ImageType = mime, ImageUrl = url, IsZeroWidth = BttvZeroWidth.Contains(name) });
}
return returnList;
@@ -550,8 +549,7 @@ public static async Task> GetChatBadges(List comments,
foreach (var (version, data) in badge.versions)
{
- string[] id_parts = data.url.Split('/');
- string id = id_parts[id_parts.Length - 2];
+ string id = data.url.Split('/')[^2];
byte[] bytes = await GetImage(badgeFolder, data.url, id, "2", "png", cancellationToken);
versions.Add(version, new ChatBadgeData
{
@@ -637,7 +635,7 @@ public static async Task> GetEmojis(string cacheFol
return returnCache;
}
- public static async Task> GetBits(List comments, string cacheFolder, string channel_id = "", EmbeddedData embeddedData = null, bool offline = false, CancellationToken cancellationToken = default)
+ public static async Task> GetBits(List comments, string cacheFolder, string channelId = "", EmbeddedData embeddedData = null, bool offline = false, CancellationToken cancellationToken = default)
{
List returnList = new List();
List alreadyAdded = new List();
@@ -669,7 +667,7 @@ public static async Task> GetBits(List comments, strin
{
RequestUri = new Uri("https://gql.twitch.tv/gql"),
Method = HttpMethod.Post,
- Content = new StringContent("{\"query\":\"query{cheerConfig{groups{nodes{id, prefix, tiers{bits}}, templateURL}},user(id:\\\"" + channel_id + "\\\"){cheer{cheerGroups{nodes{id,prefix,tiers{bits}},templateURL}}}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
+ Content = new StringContent("{\"query\":\"query{cheerConfig{groups{nodes{id, prefix, tiers{bits}}, templateURL}},user(id:\\\"" + channelId + "\\\"){cheer{cheerGroups{nodes{id,prefix,tiers{bits}},templateURL}}}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
};
request.Headers.Add("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko");
using var cheerResponseMessage = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
@@ -860,7 +858,7 @@ public static async Task GetUserInfo(List idList)
if (!Directory.Exists(cachePath))
CreateDirectory(cachePath);
- string filePath = Path.Combine(cachePath, imageId + "_" + imageScale + "." + imageType);
+ string filePath = Path.Combine(cachePath!, imageId + "_" + imageScale + "." + imageType);
if (File.Exists(filePath))
{
try
@@ -906,7 +904,7 @@ public static async Task GetUserInfo(List idList)
//Let's save this image to the cache
try
{
- using FileStream stream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
+ await using var stream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
await stream.WriteAsync(imageBytes, cancellationToken);
}
catch { }
@@ -928,5 +926,4 @@ public static async Task GetVideoChapters(int videoId)
return await response.Content.ReadFromJsonAsync();
}
}
-
}
\ No newline at end of file
diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs
index b6c5b2eb..d11f5482 100644
--- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs
+++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs
@@ -48,6 +48,4 @@ public class GqlClipSearchResponse
public ClipSearchData data { get; set; }
public Extensions extensions { get; set; }
}
-
-
}
diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
index 917c2016..333fdbef 100644
--- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
@@ -29,7 +29,7 @@ namespace TwitchDownloaderWPF
///
public partial class PageVodDownload : Page
{
- public Dictionary videoQualties = new();
+ public readonly Dictionary videoQualities = new();
public int currentVideoId;
public DateTime currentVideoTime;
public TimeSpan vodLength;
@@ -116,7 +116,7 @@ private async Task GetVideoInfo()
}
comboQuality.Items.Clear();
- videoQualties.Clear();
+ videoQualities.Clear();
string[] playlist = await taskPlaylist;
if (playlist[0].Contains("vod_manifest_restricted"))
{
@@ -127,16 +127,16 @@ private async Task GetVideoInfo()
{
if (playlist[i].Contains("#EXT-X-MEDIA"))
{
- string lastPart = playlist[i].Substring(playlist[i].IndexOf("NAME=\"") + 6);
+ string lastPart = playlist[i].Substring(playlist[i].IndexOf("NAME=\"", StringComparison.Ordinal) + 6);
string stringQuality = lastPart.Substring(0, lastPart.IndexOf('"'));
- var bandwidthStartIndex = playlist[i + 1].IndexOf("BANDWIDTH=") + 10;
+ var bandwidthStartIndex = playlist[i + 1].IndexOf("BANDWIDTH=", StringComparison.Ordinal) + 10;
var bandwidthEndIndex = playlist[i + 1].IndexOf(',') - bandwidthStartIndex;
int.TryParse(playlist[i + 1].Substring(bandwidthStartIndex, bandwidthEndIndex), out var bandwidth);
- if (!videoQualties.ContainsKey(stringQuality))
+ if (!videoQualities.ContainsKey(stringQuality))
{
- videoQualties.Add(stringQuality, (playlist[i + 2], bandwidth));
+ videoQualities.Add(stringQuality, (playlist[i + 2], bandwidth));
comboQuality.Items.Add(stringQuality);
}
}
@@ -242,7 +242,7 @@ private void UpdateVideoSizeEstimates()
{
var qualityWithSize = (string)comboQuality.Items[i];
var quality = GetQualityWithoutSize(qualityWithSize).ToString();
- int bandwidth = videoQualties[quality].bandwidth;
+ int bandwidth = videoQualities[quality].bandwidth;
var sizeInBytes = VideoSizeEstimator.EstimateVideoSize(bandwidth, cropStart, cropEnd);
var newVideoSize = VideoSizeEstimator.StringifyByteCount(sizeInBytes);
diff --git a/TwitchDownloaderWPF/WindowMassDownload.xaml b/TwitchDownloaderWPF/WindowMassDownload.xaml
index ac9b3ad5..0d84b420 100644
--- a/TwitchDownloaderWPF/WindowMassDownload.xaml
+++ b/TwitchDownloaderWPF/WindowMassDownload.xaml
@@ -30,7 +30,7 @@
-
+
@@ -47,7 +47,7 @@
-
+
From 485ca2ae36115b32055d5186437bcd756f438793 Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 22 Sep 2023 01:49:41 -0400
Subject: [PATCH 20/50] Include VOD description in video metadata if present
(#822)
* Add video description to fetch query
* Serialize video description if applicable
* Sanitize line feeds
---
TwitchDownloaderCore/Tools/FfmpegMetadata.cs | 20 +++++++++++++------
TwitchDownloaderCore/TwitchHelper.cs | 2 +-
.../TwitchObjects/Gql/GqlVideoResponse.cs | 1 +
TwitchDownloaderCore/VideoDownloader.cs | 5 +++--
4 files changed, 19 insertions(+), 9 deletions(-)
diff --git a/TwitchDownloaderCore/Tools/FfmpegMetadata.cs b/TwitchDownloaderCore/Tools/FfmpegMetadata.cs
index f8aa3851..2e7959b8 100644
--- a/TwitchDownloaderCore/Tools/FfmpegMetadata.cs
+++ b/TwitchDownloaderCore/Tools/FfmpegMetadata.cs
@@ -13,27 +13,34 @@ public static class FfmpegMetadata
{
private const string LINE_FEED = "\u000A";
- public static async Task SerializeAsync(string filePath, string streamerName, string videoId, string videoTitle, DateTime videoCreation, int viewCount, double startOffsetSeconds = default, List videoMomentEdges = default, CancellationToken cancellationToken = default)
+ public static async Task SerializeAsync(string filePath, string streamerName, string videoId, string videoTitle, DateTime videoCreation, int viewCount, string videoDescription = null,
+ double startOffsetSeconds = 0, List videoMomentEdges = null, CancellationToken cancellationToken = default)
{
await using var fs = new FileStream(filePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None);
await using var sw = new StreamWriter(fs) { NewLine = LINE_FEED };
- await SerializeGlobalMetadata(sw, streamerName, videoId, videoTitle, videoCreation, viewCount);
+ await SerializeGlobalMetadata(sw, streamerName, videoId, videoTitle, videoCreation, viewCount, videoDescription);
await fs.FlushAsync(cancellationToken);
await SerializeChapters(sw, videoMomentEdges, startOffsetSeconds);
await fs.FlushAsync(cancellationToken);
}
- private static async Task SerializeGlobalMetadata(StreamWriter sw, string streamerName, string videoId, string videoTitle, DateTime videoCreation, int viewCount)
+ private static async Task SerializeGlobalMetadata(StreamWriter sw, string streamerName, string videoId, string videoTitle, DateTime videoCreation, int viewCount, string videoDescription)
{
await sw.WriteLineAsync(";FFMETADATA1");
await sw.WriteLineAsync($"title={SanitizeKeyValue(videoTitle)} ({SanitizeKeyValue(videoId)})");
await sw.WriteLineAsync($"artist={SanitizeKeyValue(streamerName)}");
await sw.WriteLineAsync($"date={videoCreation:yyyy}"); // The 'date' key becomes 'year' in most formats
- await sw.WriteLineAsync(@$"comment=Originally aired: {SanitizeKeyValue(videoCreation.ToString("u"))}\");
+ await sw.WriteAsync(@"comment=");
+ if (!string.IsNullOrWhiteSpace(videoDescription))
+ {
+ await sw.WriteLineAsync(@$"{SanitizeKeyValue(videoDescription.TrimEnd())}\");
+ await sw.WriteLineAsync(@"------------------------\");
+ }
+ await sw.WriteLineAsync(@$"Originally aired: {SanitizeKeyValue(videoCreation.ToString("u"))}\");
await sw.WriteLineAsync(@$"Video id: {SanitizeKeyValue(videoId)}\");
- await sw.WriteLineAsync($"Views: {viewCount}");
+ await sw.WriteLineAsync(@$"Views: {viewCount}");
}
private static async Task SerializeChapters(StreamWriter sw, List videoMomentEdges, double startOffsetSeconds)
@@ -71,7 +78,7 @@ private static string SanitizeKeyValue(string str)
return str;
}
- if (str.AsSpan().IndexOfAny(@"=;#\") == -1)
+ if (str.AsSpan().IndexOfAny(@$"=;#\{LINE_FEED}") == -1)
{
return str;
}
@@ -81,6 +88,7 @@ private static string SanitizeKeyValue(string str)
.Replace(";", @"\;")
.Replace("#", @"\#")
.Replace(@"\", @"\\")
+ .Replace(LINE_FEED, $@"\{LINE_FEED}")
.ToString();
}
}
diff --git a/TwitchDownloaderCore/TwitchHelper.cs b/TwitchDownloaderCore/TwitchHelper.cs
index 81918791..bc7ee73a 100644
--- a/TwitchDownloaderCore/TwitchHelper.cs
+++ b/TwitchDownloaderCore/TwitchHelper.cs
@@ -30,7 +30,7 @@ public static async Task GetVideoInfo(int videoId)
{
RequestUri = new Uri("https://gql.twitch.tv/gql"),
Method = HttpMethod.Post,
- Content = new StringContent("{\"query\":\"query{video(id:\\\"" + videoId + "\\\"){title,thumbnailURLs(height:180,width:320),createdAt,lengthSeconds,owner{id,displayName},viewCount,game{id,displayName}}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
+ Content = new StringContent("{\"query\":\"query{video(id:\\\"" + videoId + "\\\"){title,thumbnailURLs(height:180,width:320),createdAt,lengthSeconds,owner{id,displayName},viewCount,game{id,displayName},description}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
};
request.Headers.Add("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko");
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
index d1489901..4cfd4127 100644
--- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
+++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
@@ -24,6 +24,7 @@ public class VideoInfo
public VideoOwner owner { get; set; }
public int viewCount { get; set; }
public VideoGame game { get; set; }
+ public string description { get; set; }
}
public class VideoData
diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs
index 43f080fc..0a2c105e 100644
--- a/TwitchDownloaderCore/VideoDownloader.cs
+++ b/TwitchDownloaderCore/VideoDownloader.cs
@@ -96,8 +96,9 @@ public async Task DownloadAsync(CancellationToken cancellationToken)
double seekDuration = Math.Round(downloadOptions.CropEndingTime - seekTime);
string metadataPath = Path.Combine(downloadFolder, "metadata.txt");
- await FfmpegMetadata.SerializeAsync(metadataPath, videoInfoResponse.data.video.owner.displayName, downloadOptions.Id.ToString(), videoInfoResponse.data.video.title,
- videoInfoResponse.data.video.createdAt, videoInfoResponse.data.video.viewCount, startOffset, videoChapterResponse.data.video.moments.edges, cancellationToken);
+ VideoInfo videoInfo = videoInfoResponse.data.video;
+ await FfmpegMetadata.SerializeAsync(metadataPath, videoInfo.owner.displayName, downloadOptions.Id.ToString(), videoInfo.title, videoInfo.createdAt, videoInfo.viewCount,
+ videoInfo.description, startOffset, videoChapterResponse.data.video.moments.edges, cancellationToken);
var finalizedFileDirectory = Directory.GetParent(Path.GetFullPath(downloadOptions.Filename))!;
if (!finalizedFileDirectory.Exists)
From 71b08d48936d77bc4dffbe4a58e13d0e486f4e8e Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 22 Sep 2023 19:35:43 -0400
Subject: [PATCH 21/50] Add video description to chat json files
---
TwitchDownloaderCore/ChatDownloader.cs | 1 +
TwitchDownloaderCore/TwitchObjects/ChatRoot.cs | 1 +
2 files changed, 2 insertions(+)
diff --git a/TwitchDownloaderCore/ChatDownloader.cs b/TwitchDownloaderCore/ChatDownloader.cs
index a0932324..74b4c070 100644
--- a/TwitchDownloaderCore/ChatDownloader.cs
+++ b/TwitchDownloaderCore/ChatDownloader.cs
@@ -279,6 +279,7 @@ public async Task DownloadAsync(IProgress progress, Cancellation
chatRoot.streamer.name = videoInfoResponse.data.video.owner.displayName;
chatRoot.streamer.id = int.Parse(videoInfoResponse.data.video.owner.id);
+ chatRoot.video.description = videoInfoResponse.data.video.description;
videoTitle = videoInfoResponse.data.video.title;
videoCreatedAt = videoInfoResponse.data.video.createdAt;
videoStart = downloadOptions.CropBeginning ? downloadOptions.CropBeginningTime : 0.0;
diff --git a/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs b/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs
index 7413b469..36fa5b73 100644
--- a/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs
+++ b/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs
@@ -193,6 +193,7 @@ public class VideoChapter
public class Video
{
public string title { get; set; }
+ public string description { get; set; }
public string id { get; set; }
public DateTime created_at { get; set; }
public double start { get; set; }
From 9f4a72bb305ff0c4d17028bf98e3e53ba16bdcaa Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 22 Sep 2023 22:15:30 -0400
Subject: [PATCH 22/50] Increase "old" directory threshold to 7 days
---
TwitchDownloaderCore/TwitchHelper.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/TwitchDownloaderCore/TwitchHelper.cs b/TwitchDownloaderCore/TwitchHelper.cs
index bc7ee73a..bdd0db78 100644
--- a/TwitchDownloaderCore/TwitchHelper.cs
+++ b/TwitchDownloaderCore/TwitchHelper.cs
@@ -785,7 +785,7 @@ private static bool DeleteOldDirectory(string directory, ReadOnlySpan dire
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
const int TWENTY_FOUR_HOURS_MILLIS = 86_400_000;
- if (currentTime - downloadTime > TWENTY_FOUR_HOURS_MILLIS)
+ if (currentTime - downloadTime > TWENTY_FOUR_HOURS_MILLIS * 7)
{
try
{
From 61d09158e5b6533decb91079a514f7175f5f8da3 Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Sun, 24 Sep 2023 18:43:04 -0400
Subject: [PATCH 23/50] Fix some theme related crashes (#831)
* Fix crash when app themes fail to extract
* More appropriate name
* Catch potential FormatExceptions when converting theme color strings to brushes
---
...del.cs => ThemeResourceDictionaryModel.cs} | 2 +-
.../Services/DefaultThemeService.cs | 7 ++++---
TwitchDownloaderWPF/Services/ThemeService.cs | 20 +++++++++++++++----
TwitchDownloaderWPF/WindowSettings.xaml.cs | 15 ++++++++------
4 files changed, 30 insertions(+), 14 deletions(-)
rename TwitchDownloaderWPF/Models/{ResourceDictionaryModel.cs => ThemeResourceDictionaryModel.cs} (92%)
diff --git a/TwitchDownloaderWPF/Models/ResourceDictionaryModel.cs b/TwitchDownloaderWPF/Models/ThemeResourceDictionaryModel.cs
similarity index 92%
rename from TwitchDownloaderWPF/Models/ResourceDictionaryModel.cs
rename to TwitchDownloaderWPF/Models/ThemeResourceDictionaryModel.cs
index 7315dd2c..6e834d08 100644
--- a/TwitchDownloaderWPF/Models/ResourceDictionaryModel.cs
+++ b/TwitchDownloaderWPF/Models/ThemeResourceDictionaryModel.cs
@@ -4,7 +4,7 @@
namespace TwitchDownloaderWPF.Models
{
[XmlRoot(ElementName = "ResourceDictionary", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation")]
- public class ResourceDictionaryModel
+ public class ThemeResourceDictionaryModel
{
[XmlElement(ElementName = "SolidColorBrush", Namespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation")]
public List SolidColorBrush { get; set; }
diff --git a/TwitchDownloaderWPF/Services/DefaultThemeService.cs b/TwitchDownloaderWPF/Services/DefaultThemeService.cs
index 325b3c84..c766f06a 100644
--- a/TwitchDownloaderWPF/Services/DefaultThemeService.cs
+++ b/TwitchDownloaderWPF/Services/DefaultThemeService.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System;
+using System.IO;
using System.Linq;
using System.Reflection;
@@ -25,11 +26,11 @@ public static bool WriteIncludedThemes()
try
{
- using var fs = new FileStream(themeFullPath, FileMode.Create, FileAccess.Write, FileShare.Read, 4096);
+ using var fs = new FileStream(themeFullPath, FileMode.Create, FileAccess.Write, FileShare.Read);
themeStream.CopyTo(fs);
}
catch (IOException) { }
- catch (System.UnauthorizedAccessException) { }
+ catch (UnauthorizedAccessException) { }
catch (System.Security.SecurityException) { }
if (!File.Exists(themeFullPath))
diff --git a/TwitchDownloaderWPF/Services/ThemeService.cs b/TwitchDownloaderWPF/Services/ThemeService.cs
index 477f3a2e..a8c80d86 100644
--- a/TwitchDownloaderWPF/Services/ThemeService.cs
+++ b/TwitchDownloaderWPF/Services/ThemeService.cs
@@ -28,7 +28,12 @@ public ThemeService(App app, WindowsThemeService windowsThemeService)
{
if (!Directory.Exists("Themes"))
{
- Directory.CreateDirectory("Themes");
+ try
+ {
+ Directory.CreateDirectory("Themes");
+ }
+ catch (IOException) { }
+ catch (UnauthorizedAccessException) { }
}
if (!DefaultThemeService.WriteIncludedThemes())
@@ -116,6 +121,9 @@ public void SetTitleBarTheme(WindowCollection windows)
private void ChangeThemePath(string newTheme)
{
+ if (!Directory.Exists("Themes"))
+ return;
+
var themeFiles = Directory.GetFiles("Themes", "*.xaml");
var newThemeString = Path.Combine("Themes", $"{newTheme}.xaml");
@@ -124,13 +132,17 @@ private void ChangeThemePath(string newTheme)
if (!newThemeString.Equals(themeFile, StringComparison.OrdinalIgnoreCase))
continue;
- var xmlReader = new XmlSerializer(typeof(ResourceDictionaryModel));
+ var xmlReader = new XmlSerializer(typeof(ThemeResourceDictionaryModel));
using var streamReader = new StreamReader(themeFile);
- var themeValues = (ResourceDictionaryModel)xmlReader.Deserialize(streamReader)!;
+ var themeValues = (ThemeResourceDictionaryModel)xmlReader.Deserialize(streamReader)!;
foreach (var solidBrush in themeValues.SolidColorBrush)
{
- _wpfApplication.Resources[solidBrush.Key] = (SolidColorBrush)new BrushConverter().ConvertFrom(solidBrush.Color);
+ try
+ {
+ _wpfApplication.Resources[solidBrush.Key] = (SolidColorBrush)new BrushConverter().ConvertFrom(solidBrush.Color);
+ }
+ catch (FormatException) { }
}
foreach (var boolean in themeValues.Boolean)
diff --git a/TwitchDownloaderWPF/WindowSettings.xaml.cs b/TwitchDownloaderWPF/WindowSettings.xaml.cs
index dd4e7ab5..3ab1ac26 100644
--- a/TwitchDownloaderWPF/WindowSettings.xaml.cs
+++ b/TwitchDownloaderWPF/WindowSettings.xaml.cs
@@ -50,14 +50,17 @@ private void Window_Initialized(object sender, EventArgs e)
CheckThrottleEnabled.IsChecked = Settings.Default.DownloadThrottleEnabled;
radioTimeFormatUTC.IsChecked = Settings.Default.UTCVideoTime;
- // Setup theme dropdown
- comboTheme.Items.Add("System"); // Cannot be localized
- string[] themeFiles = Directory.GetFiles("Themes", "*.xaml");
- foreach (string themeFile in themeFiles)
+ if (Directory.Exists("Themes"))
{
- comboTheme.Items.Add(Path.GetFileNameWithoutExtension(themeFile));
+ // Setup theme dropdown
+ comboTheme.Items.Add("System"); // Cannot be localized
+ string[] themeFiles = Directory.GetFiles("Themes", "*.xaml");
+ foreach (string themeFile in themeFiles)
+ {
+ comboTheme.Items.Add(Path.GetFileNameWithoutExtension(themeFile));
+ }
+ comboTheme.SelectedItem = Settings.Default.GuiTheme;
}
- comboTheme.SelectedItem = Settings.Default.GuiTheme;
// Setup culture dropdown
foreach (var culture in AvailableCultures.All)
From f8f2459982567ad46d3c595fd39d5037ff92ed75 Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Sun, 24 Sep 2023 20:29:36 -0400
Subject: [PATCH 24/50] Nuget upgrade (#804)
* Bump SkiaSharp to 2.88.5
* Bump HarfBuzzSharp to 2.8.2.5
* Bump Autoupdater.NET to 1.8.4
* Experiment with removing dedicated AlpineLinux SkiaSharp package
* Bump SkiaSharp to 2.88.6 & HarfBuzzSharp to 7.3.0
---
TwitchDownloaderCLI/TwitchDownloaderCLI.csproj | 1 -
TwitchDownloaderCore/TwitchDownloaderCore.csproj | 12 ++++++------
TwitchDownloaderWPF/TwitchDownloaderWPF.csproj | 2 +-
3 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
index c00a7611..e9dd74df 100644
--- a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
+++ b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
@@ -15,7 +15,6 @@
-
diff --git a/TwitchDownloaderCore/TwitchDownloaderCore.csproj b/TwitchDownloaderCore/TwitchDownloaderCore.csproj
index 48856a1e..05258a5b 100644
--- a/TwitchDownloaderCore/TwitchDownloaderCore.csproj
+++ b/TwitchDownloaderCore/TwitchDownloaderCore.csproj
@@ -23,13 +23,13 @@
-
+
+
-
-
-
-
-
+
+
+
+
diff --git a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
index aba5f4b6..c2f05537 100644
--- a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
+++ b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
@@ -63,7 +63,7 @@
-
+
From c2eb143d2b2bd10e620b35907c45deea3c2ee7d2 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Sun, 24 Sep 2023 21:04:32 -0400
Subject: [PATCH 25/50] Fix duplicate newlines and double spaces in video
descriptions
---
TwitchDownloaderCore/ChatDownloader.cs | 2 +-
TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs | 4 ++++
TwitchDownloaderCore/VideoDownloader.cs | 2 +-
3 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/TwitchDownloaderCore/ChatDownloader.cs b/TwitchDownloaderCore/ChatDownloader.cs
index 74b4c070..b6029c7a 100644
--- a/TwitchDownloaderCore/ChatDownloader.cs
+++ b/TwitchDownloaderCore/ChatDownloader.cs
@@ -279,7 +279,7 @@ public async Task DownloadAsync(IProgress progress, Cancellation
chatRoot.streamer.name = videoInfoResponse.data.video.owner.displayName;
chatRoot.streamer.id = int.Parse(videoInfoResponse.data.video.owner.id);
- chatRoot.video.description = videoInfoResponse.data.video.description;
+ chatRoot.video.description = videoInfoResponse.data.video.description.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd();
videoTitle = videoInfoResponse.data.video.title;
videoCreatedAt = videoInfoResponse.data.video.createdAt;
videoStart = downloadOptions.CropBeginning ? downloadOptions.CropBeginningTime : 0.0;
diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
index 4cfd4127..f6d1f0c4 100644
--- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
+++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
@@ -24,6 +24,10 @@ public class VideoInfo
public VideoOwner owner { get; set; }
public int viewCount { get; set; }
public VideoGame game { get; set; }
+ ///
+ /// Some values, such as newlines, are repeated twice for some reason.
+ /// This can be filtered out with: .Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd()
+ ///
public string description { get; set; }
}
diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs
index 0a2c105e..eec2281c 100644
--- a/TwitchDownloaderCore/VideoDownloader.cs
+++ b/TwitchDownloaderCore/VideoDownloader.cs
@@ -98,7 +98,7 @@ public async Task DownloadAsync(CancellationToken cancellationToken)
string metadataPath = Path.Combine(downloadFolder, "metadata.txt");
VideoInfo videoInfo = videoInfoResponse.data.video;
await FfmpegMetadata.SerializeAsync(metadataPath, videoInfo.owner.displayName, downloadOptions.Id.ToString(), videoInfo.title, videoInfo.createdAt, videoInfo.viewCount,
- videoInfo.description, startOffset, videoChapterResponse.data.video.moments.edges, cancellationToken);
+ videoInfo.description.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd(), startOffset, videoChapterResponse.data.video.moments.edges, cancellationToken);
var finalizedFileDirectory = Directory.GetParent(Path.GetFullPath(downloadOptions.Filename))!;
if (!finalizedFileDirectory.Exists)
From 0064ec6f7260b616b733bd11501692c923288e7a Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Sat, 30 Sep 2023 00:26:03 -0400
Subject: [PATCH 26/50] Quality of life GUI improvements (#833)
* Use URIs so thumbnails are actually cached for reuse
* Add design time DataContext to PageQueue and WindowMassDownload
* Add loading animation to WindowMassDownload
* Fix wrong border color on bandwidth throttle checkbox
* Combine CheckThrottleEnabled Checked functions
* Tabs -> spaces
* Apparently this wasn't entirely necessary
* Increase code readability
* Use ThumbnailService in WindowUrlList & add more video metadata
* Start cleaning up WindowQueueOptions
* Use Nullable.GetValueOrDefault everywhere & general cleanup
* Refactor WindowQueueOptions to be a little bit easier to follow
---
TwitchDownloaderWPF/PageChatDownload.xaml.cs | 44 +-
TwitchDownloaderWPF/PageChatRender.xaml.cs | 98 ++--
TwitchDownloaderWPF/PageChatUpdate.xaml.cs | 40 +-
TwitchDownloaderWPF/PageClipDownload.xaml.cs | 16 +-
TwitchDownloaderWPF/PageQueue.xaml | 5 +-
TwitchDownloaderWPF/PageQueue.xaml.cs | 8 +-
TwitchDownloaderWPF/PageVodDownload.xaml.cs | 32 +-
.../Services/ThumbnailService.cs | 38 +-
TwitchDownloaderWPF/WindowMassDownload.xaml | 28 +-
.../WindowMassDownload.xaml.cs | 130 +++--
.../WindowQueueOptions.xaml.cs | 493 ++++++++++--------
TwitchDownloaderWPF/WindowRangeSelect.xaml.cs | 33 +-
TwitchDownloaderWPF/WindowSettings.xaml | 2 +-
TwitchDownloaderWPF/WindowSettings.xaml.cs | 17 +-
TwitchDownloaderWPF/WindowUrlList.xaml.cs | 116 +++--
15 files changed, 576 insertions(+), 524 deletions(-)
diff --git a/TwitchDownloaderWPF/PageChatDownload.xaml.cs b/TwitchDownloaderWPF/PageChatDownload.xaml.cs
index 9f59d6ea..08fa9ac7 100644
--- a/TwitchDownloaderWPF/PageChatDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageChatDownload.xaml.cs
@@ -117,20 +117,14 @@ private async Task GetVideoInfo()
{
GqlVideoResponse videoInfo = await TwitchHelper.GetVideoInfo(int.Parse(downloadId));
- try
- {
- string thumbUrl = videoInfo.data.video.thumbnailURLs.FirstOrDefault();
- imgThumbnail.Source = await ThumbnailService.GetThumb(thumbUrl);
- }
- catch
+ var thumbUrl = videoInfo.data.video.thumbnailURLs.FirstOrDefault();
+ if (!ThumbnailService.TryGetThumb(thumbUrl, out var image))
{
AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
- var (success, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
- if (success)
- {
- imgThumbnail.Source = image;
- }
+ _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out image);
}
+ imgThumbnail.Source = image;
+
vodLength = TimeSpan.FromSeconds(videoInfo.data.video.lengthSeconds);
textTitle.Text = videoInfo.data.video.title;
textStreamer.Text = videoInfo.data.video.owner.displayName;
@@ -169,20 +163,14 @@ private async Task GetVideoInfo()
string clipId = downloadId;
GqlClipResponse clipInfo = await TwitchHelper.GetClipInfo(clipId);
- try
- {
- string thumbUrl = clipInfo.data.clip.thumbnailURL;
- imgThumbnail.Source = await ThumbnailService.GetThumb(thumbUrl);
- }
- catch
+ var thumbUrl = clipInfo.data.clip.thumbnailURL;
+ if (!ThumbnailService.TryGetThumb(thumbUrl, out var image))
{
AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
- var (success, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
- if (success)
- {
- imgThumbnail.Source = image;
- }
+ _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out image);
}
+ imgThumbnail.Source = image;
+
TimeSpan clipLength = TimeSpan.FromSeconds(clipInfo.data.clip.durationSeconds);
textStreamer.Text = clipInfo.data.clip.broadcaster.displayName;
var clipCreatedAt = clipInfo.data.clip.createdAt;
@@ -253,10 +241,10 @@ public ChatDownloadOptions GetOptions(string filename)
else if (radioCompressionGzip.IsChecked == true)
options.Compression = ChatCompression.Gzip;
- options.EmbedData = (bool)checkEmbed.IsChecked;
- options.BttvEmotes = (bool)checkBttvEmbed.IsChecked;
- options.FfzEmotes = (bool)checkFfzEmbed.IsChecked;
- options.StvEmotes = (bool)checkStvEmbed.IsChecked;
+ options.EmbedData = checkEmbed.IsChecked.GetValueOrDefault();
+ options.BttvEmotes = checkBttvEmbed.IsChecked.GetValueOrDefault();
+ options.FfzEmotes = checkFfzEmbed.IsChecked.GetValueOrDefault();
+ options.StvEmotes = checkStvEmbed.IsChecked.GetValueOrDefault();
options.Filename = filename;
options.ConnectionCount = (int)numChatDownloadConnections.Value;
return options;
@@ -573,12 +561,12 @@ private void BtnCancel_Click(object sender, RoutedEventArgs e)
private void checkCropStart_OnCheckStateChanged(object sender, RoutedEventArgs e)
{
- SetEnabledCropStart((bool)checkCropStart.IsChecked);
+ SetEnabledCropStart(checkCropStart.IsChecked.GetValueOrDefault());
}
private void checkCropEnd_OnCheckStateChanged(object sender, RoutedEventArgs e)
{
- SetEnabledCropEnd((bool)checkCropEnd.IsChecked);
+ SetEnabledCropEnd(checkCropEnd.IsChecked.GetValueOrDefault());
}
diff --git a/TwitchDownloaderWPF/PageChatRender.xaml.cs b/TwitchDownloaderWPF/PageChatRender.xaml.cs
index 1077e42c..08bd436b 100644
--- a/TwitchDownloaderWPF/PageChatRender.xaml.cs
+++ b/TwitchDownloaderWPF/PageChatRender.xaml.cs
@@ -96,13 +96,13 @@ public ChatRenderOptions GetOptions(string filename)
InputFile = textJson.Text,
BackgroundColor = backgroundColor,
AlternateBackgroundColor = altBackgroundColor,
- AlternateMessageBackgrounds = (bool)checkAlternateMessageBackgrounds.IsChecked,
+ AlternateMessageBackgrounds = checkAlternateMessageBackgrounds.IsChecked.GetValueOrDefault(),
ChatHeight = int.Parse(textHeight.Text),
ChatWidth = int.Parse(textWidth.Text),
- BttvEmotes = (bool)checkBTTV.IsChecked,
- FfzEmotes = (bool)checkFFZ.IsChecked,
- StvEmotes = (bool)checkSTV.IsChecked,
- Outline = (bool)checkOutline.IsChecked,
+ BttvEmotes = checkBTTV.IsChecked.GetValueOrDefault(),
+ FfzEmotes = checkFFZ.IsChecked.GetValueOrDefault(),
+ StvEmotes = checkSTV.IsChecked.GetValueOrDefault(),
+ Outline = checkOutline.IsChecked.GetValueOrDefault(),
Font = (string)comboFont.SelectedItem,
FontSize = numFontSize.Value,
UpdateRate = double.Parse(textUpdateTime.Text, CultureInfo.CurrentCulture),
@@ -118,22 +118,22 @@ public ChatRenderOptions GetOptions(string filename)
VerticalSpacingScale = double.Parse(textVerticalScale.Text, CultureInfo.CurrentCulture),
IgnoreUsersArray = textIgnoreUsersList.Text.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries),
BannedWordsArray = textBannedWordsList.Text.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries),
- Timestamp = (bool)checkTimestamp.IsChecked,
+ Timestamp = checkTimestamp.IsChecked.GetValueOrDefault(),
MessageColor = messageColor,
Framerate = int.Parse(textFramerate.Text),
InputArgs = CheckRenderSharpening.IsChecked == true ? textFfmpegInput.Text + " -filter_complex \"smartblur=lr=1:ls=-1.0\"" : textFfmpegInput.Text,
OutputArgs = textFfmpegOutput.Text,
MessageFontStyle = SKFontStyle.Normal,
UsernameFontStyle = SKFontStyle.Bold,
- GenerateMask = (bool)checkMask.IsChecked,
+ GenerateMask = checkMask.IsChecked.GetValueOrDefault(),
OutlineSize = 4 * double.Parse(textOutlineScale.Text, CultureInfo.CurrentCulture),
FfmpegPath = "ffmpeg",
TempFolder = Settings.Default.TempPath,
- SubMessages = (bool)checkSub.IsChecked,
- ChatBadges = (bool)checkBadge.IsChecked,
- Offline = (bool)checkOffline.IsChecked,
+ SubMessages = checkSub.IsChecked.GetValueOrDefault(),
+ ChatBadges = checkBadge.IsChecked.GetValueOrDefault(),
+ Offline = checkOffline.IsChecked.GetValueOrDefault(),
AllowUnlistedEmotes = true,
- DisperseCommentOffsets = (bool)checkDispersion.IsChecked,
+ DisperseCommentOffsets = checkDispersion.IsChecked.GetValueOrDefault(),
LogFfmpegOutput = true
};
if (RadioEmojiNotoColor.IsChecked == true)
@@ -287,29 +287,29 @@ private void ComboFormatOnSelectionChanged(object sender, SelectionChangedEventA
public void SaveSettings()
{
Settings.Default.Font = comboFont.SelectedItem.ToString();
- Settings.Default.Outline = (bool)checkOutline.IsChecked;
- Settings.Default.Timestamp = (bool)checkTimestamp.IsChecked;
- Settings.Default.BackgroundColorR = colorBackground.SelectedColor.Value.R;
- Settings.Default.BackgroundColorG = colorBackground.SelectedColor.Value.G;
- Settings.Default.BackgroundColorB = colorBackground.SelectedColor.Value.B;
- Settings.Default.BackgroundColorA = colorBackground.SelectedColor.Value.A;
- Settings.Default.AlternateBackgroundColorR = colorAlternateBackground.SelectedColor.Value.R;
- Settings.Default.AlternateBackgroundColorG = colorAlternateBackground.SelectedColor.Value.G;
- Settings.Default.AlternateBackgroundColorB = colorAlternateBackground.SelectedColor.Value.B;
- Settings.Default.AlternateBackgroundColorA = colorAlternateBackground.SelectedColor.Value.A;
- Settings.Default.FFZEmotes = (bool)checkFFZ.IsChecked;
- Settings.Default.BTTVEmotes = (bool)checkBTTV.IsChecked;
- Settings.Default.STVEmotes = (bool)checkSTV.IsChecked;
- Settings.Default.FontColorR = colorFont.SelectedColor.Value.R;
- Settings.Default.FontColorG = colorFont.SelectedColor.Value.G;
- Settings.Default.FontColorB = colorFont.SelectedColor.Value.B;
- Settings.Default.GenerateMask = (bool)checkMask.IsChecked;
- Settings.Default.ChatRenderSharpening = (bool)CheckRenderSharpening.IsChecked;
- Settings.Default.SubMessages = (bool)checkSub.IsChecked;
- Settings.Default.ChatBadges = (bool)checkBadge.IsChecked;
- Settings.Default.Offline = (bool)checkOffline.IsChecked;
- Settings.Default.DisperseCommentOffsets = (bool)checkDispersion.IsChecked;
- Settings.Default.AlternateMessageBackgrounds = (bool)checkAlternateMessageBackgrounds.IsChecked;
+ Settings.Default.Outline = checkOutline.IsChecked.GetValueOrDefault();
+ Settings.Default.Timestamp = checkTimestamp.IsChecked.GetValueOrDefault();
+ Settings.Default.BackgroundColorR = colorBackground.SelectedColor.GetValueOrDefault().R;
+ Settings.Default.BackgroundColorG = colorBackground.SelectedColor.GetValueOrDefault().G;
+ Settings.Default.BackgroundColorB = colorBackground.SelectedColor.GetValueOrDefault().B;
+ Settings.Default.BackgroundColorA = colorBackground.SelectedColor.GetValueOrDefault().A;
+ Settings.Default.AlternateBackgroundColorR = colorAlternateBackground.SelectedColor.GetValueOrDefault().R;
+ Settings.Default.AlternateBackgroundColorG = colorAlternateBackground.SelectedColor.GetValueOrDefault().G;
+ Settings.Default.AlternateBackgroundColorB = colorAlternateBackground.SelectedColor.GetValueOrDefault().B;
+ Settings.Default.AlternateBackgroundColorA = colorAlternateBackground.SelectedColor.GetValueOrDefault().A;
+ Settings.Default.FFZEmotes = checkFFZ.IsChecked.GetValueOrDefault();
+ Settings.Default.BTTVEmotes = checkBTTV.IsChecked.GetValueOrDefault();
+ Settings.Default.STVEmotes = checkSTV.IsChecked.GetValueOrDefault();
+ Settings.Default.FontColorR = colorFont.SelectedColor.GetValueOrDefault().R;
+ Settings.Default.FontColorG = colorFont.SelectedColor.GetValueOrDefault().G;
+ Settings.Default.FontColorB = colorFont.SelectedColor.GetValueOrDefault().B;
+ Settings.Default.GenerateMask = checkMask.IsChecked.GetValueOrDefault();
+ Settings.Default.ChatRenderSharpening = CheckRenderSharpening.IsChecked.GetValueOrDefault();
+ Settings.Default.SubMessages = checkSub.IsChecked.GetValueOrDefault();
+ Settings.Default.ChatBadges = checkBadge.IsChecked.GetValueOrDefault();
+ Settings.Default.Offline = checkOffline.IsChecked.GetValueOrDefault();
+ Settings.Default.DisperseCommentOffsets = checkDispersion.IsChecked.GetValueOrDefault();
+ Settings.Default.AlternateMessageBackgrounds = checkAlternateMessageBackgrounds.IsChecked.GetValueOrDefault();
if (comboFormat.SelectedItem != null)
{
Settings.Default.VideoContainer = ((VideoContainer)comboFormat.SelectedItem).Name;
@@ -376,21 +376,21 @@ private bool ValidateInputs()
try
{
- int.Parse(textHeight.Text);
- int.Parse(textWidth.Text);
- double.Parse(textUpdateTime.Text, CultureInfo.CurrentCulture);
- int.Parse(textFramerate.Text);
- double.Parse(textEmoteScale.Text, CultureInfo.CurrentCulture);
- double.Parse(textBadgeScale.Text, CultureInfo.CurrentCulture);
- double.Parse(textEmojiScale.Text, CultureInfo.CurrentCulture);
- double.Parse(textVerticalScale.Text, CultureInfo.CurrentCulture);
- double.Parse(textSidePaddingScale.Text, CultureInfo.CurrentCulture);
- double.Parse(textSectionHeightScale.Text, CultureInfo.CurrentCulture);
- double.Parse(textWordSpaceScale.Text, CultureInfo.CurrentCulture);
- double.Parse(textEmoteSpaceScale.Text, CultureInfo.CurrentCulture);
- double.Parse(textAccentStrokeScale.Text, CultureInfo.CurrentCulture);
- double.Parse(textAccentIndentScale.Text, CultureInfo.CurrentCulture);
- double.Parse(textOutlineScale.Text, CultureInfo.CurrentCulture);
+ _ = int.Parse(textHeight.Text);
+ _ = int.Parse(textWidth.Text);
+ _ = double.Parse(textUpdateTime.Text, CultureInfo.CurrentCulture);
+ _ = int.Parse(textFramerate.Text);
+ _ = double.Parse(textEmoteScale.Text, CultureInfo.CurrentCulture);
+ _ = double.Parse(textBadgeScale.Text, CultureInfo.CurrentCulture);
+ _ = double.Parse(textEmojiScale.Text, CultureInfo.CurrentCulture);
+ _ = double.Parse(textVerticalScale.Text, CultureInfo.CurrentCulture);
+ _ = double.Parse(textSidePaddingScale.Text, CultureInfo.CurrentCulture);
+ _ = double.Parse(textSectionHeightScale.Text, CultureInfo.CurrentCulture);
+ _ = double.Parse(textWordSpaceScale.Text, CultureInfo.CurrentCulture);
+ _ = double.Parse(textEmoteSpaceScale.Text, CultureInfo.CurrentCulture);
+ _ = double.Parse(textAccentStrokeScale.Text, CultureInfo.CurrentCulture);
+ _ = double.Parse(textAccentIndentScale.Text, CultureInfo.CurrentCulture);
+ _ = double.Parse(textOutlineScale.Text, CultureInfo.CurrentCulture);
}
catch (Exception ex)
{
diff --git a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
index 2a064828..0acbd49e 100644
--- a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
+++ b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
@@ -119,7 +119,7 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e)
if (videoInfo.data.video == null)
{
AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail + ": " + Translations.Strings.VodExpiredOrIdCorrupt);
- var (_, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
+ _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out var image);
imgThumbnail.Source = image;
numStartHour.Maximum = 48;
@@ -135,11 +135,10 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e)
Game = videoInfo.data.video.game?.displayName;
var thumbUrl = videoInfo.data.video.thumbnailURLs.FirstOrDefault();
- var (success, image) = await ThumbnailService.TryGetThumb(thumbUrl);
- if (!success)
+ if (!ThumbnailService.TryGetThumb(thumbUrl, out var image))
{
AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
- (_, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
+ _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out image);
}
imgThumbnail.Source = image;
@@ -157,7 +156,7 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e)
if (videoInfo.data.clip.video == null)
{
AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail + ": " + Translations.Strings.VodExpiredOrIdCorrupt);
- var (_, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
+ _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out var image);
imgThumbnail.Source = image;
}
else
@@ -168,11 +167,10 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e)
Game = videoInfo.data.clip.game?.displayName;
var thumbUrl = videoInfo.data.clip.thumbnailURL;
- var (success, image) = await ThumbnailService.TryGetThumb(thumbUrl);
- if (!success)
+ if (!ThumbnailService.TryGetThumb(thumbUrl, out var image))
{
AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
- (_, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
+ _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out image);
}
imgThumbnail.Source = image;
@@ -270,22 +268,22 @@ public ChatUpdateOptions GetOptions(string outputFile)
{
ChatUpdateOptions options = new ChatUpdateOptions()
{
- EmbedMissing = (bool)checkEmbedMissing.IsChecked,
- ReplaceEmbeds = (bool)checkReplaceEmbeds.IsChecked,
- BttvEmotes = (bool)checkBttvEmbed.IsChecked,
- FfzEmotes = (bool)checkFfzEmbed.IsChecked,
- StvEmotes = (bool)checkStvEmbed.IsChecked,
+ EmbedMissing = checkEmbedMissing.IsChecked.GetValueOrDefault(),
+ ReplaceEmbeds = checkReplaceEmbeds.IsChecked.GetValueOrDefault(),
+ BttvEmotes = checkBttvEmbed.IsChecked.GetValueOrDefault(),
+ FfzEmotes = checkFfzEmbed.IsChecked.GetValueOrDefault(),
+ StvEmotes = checkStvEmbed.IsChecked.GetValueOrDefault(),
InputFile = textJson.Text,
OutputFile = outputFile,
CropBeginningTime = -1,
CropEndingTime = -1
};
- if ((bool)radioJson.IsChecked)
+ if (radioJson.IsChecked.GetValueOrDefault())
options.OutputFormat = ChatFormat.Json;
- else if ((bool)radioHTML.IsChecked)
+ else if (radioHTML.IsChecked.GetValueOrDefault())
options.OutputFormat = ChatFormat.Html;
- else if ((bool)radioText.IsChecked)
+ else if (radioText.IsChecked.GetValueOrDefault())
options.OutputFormat = ChatFormat.Text;
if (radioCompressionNone.IsChecked == true)
@@ -306,11 +304,11 @@ public ChatUpdateOptions GetOptions(string outputFile)
options.CropEndingTime = (int)Math.Round(end.TotalSeconds);
}
- if ((bool)radioTimestampUTC.IsChecked)
+ if (radioTimestampUTC.IsChecked.GetValueOrDefault())
options.TextTimestampFormat = TimestampFormat.Utc;
- else if ((bool)radioTimestampRelative.IsChecked)
+ else if (radioTimestampRelative.IsChecked.GetValueOrDefault())
options.TextTimestampFormat = TimestampFormat.Relative;
- else if ((bool)radioTimestampNone.IsChecked)
+ else if (radioTimestampNone.IsChecked.GetValueOrDefault())
options.TextTimestampFormat = TimestampFormat.None;
return options;
@@ -637,12 +635,12 @@ private void radioText_Checked(object sender, RoutedEventArgs e)
private void checkStart_OnCheckStateChanged(object sender, RoutedEventArgs e)
{
- SetEnabledCropStart((bool)checkStart.IsChecked);
+ SetEnabledCropStart(checkStart.IsChecked.GetValueOrDefault());
}
private void checkEnd_OnCheckStateChanged(object sender, RoutedEventArgs e)
{
- SetEnabledCropEnd((bool)checkEnd.IsChecked);
+ SetEnabledCropEnd(checkEnd.IsChecked.GetValueOrDefault());
}
private void MenuItemEnqueue_Click(object sender, RoutedEventArgs e)
diff --git a/TwitchDownloaderWPF/PageClipDownload.xaml.cs b/TwitchDownloaderWPF/PageClipDownload.xaml.cs
index 634147a1..6eb722b2 100644
--- a/TwitchDownloaderWPF/PageClipDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageClipDownload.xaml.cs
@@ -60,20 +60,14 @@ private async Task GetClipInfo()
GqlClipResponse clipData = taskClipInfo.Result;
- try
- {
- string thumbUrl = clipData.data.clip.thumbnailURL;
- imgThumbnail.Source = await ThumbnailService.GetThumb(thumbUrl);
- }
- catch
+ var thumbUrl = clipData.data.clip.thumbnailURL;
+ if (!ThumbnailService.TryGetThumb(thumbUrl, out var image))
{
AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
- var (success, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
- if (success)
- {
- imgThumbnail.Source = image;
- }
+ _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out image);
}
+ imgThumbnail.Source = image;
+
clipLength = TimeSpan.FromSeconds(taskClipInfo.Result.data.clip.durationSeconds);
textStreamer.Text = clipData.data.clip.broadcaster.displayName;
var clipCreatedAt = clipData.data.clip.createdAt;
diff --git a/TwitchDownloaderWPF/PageQueue.xaml b/TwitchDownloaderWPF/PageQueue.xaml
index 3641c17e..90a00e73 100644
--- a/TwitchDownloaderWPF/PageQueue.xaml
+++ b/TwitchDownloaderWPF/PageQueue.xaml
@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TwitchDownloaderWPF"
+ xmlns:task="clr-namespace:TwitchDownloaderWPF.TwitchTasks"
xmlns:lex="http://wpflocalizeextension.codeplex.com"
lex:LocalizeDictionary.DesignCulture=""
lex:ResxLocalizationProvider.DefaultAssembly="TwitchDownloaderWPF"
@@ -32,8 +33,8 @@
-
-
+
+
diff --git a/TwitchDownloaderWPF/PageQueue.xaml.cs b/TwitchDownloaderWPF/PageQueue.xaml.cs
index f59456db..d059610b 100644
--- a/TwitchDownloaderWPF/PageQueue.xaml.cs
+++ b/TwitchDownloaderWPF/PageQueue.xaml.cs
@@ -1,7 +1,5 @@
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
+using System.Collections.ObjectModel;
using System.ComponentModel;
-using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
@@ -204,7 +202,7 @@ private void btnCancelTask_Click(object sender, RoutedEventArgs e)
private void btnTaskError_Click(object sender, RoutedEventArgs e)
{
- if (!(sender is Button { DataContext: ITwitchTask task }))
+ if (sender is not Button { DataContext: ITwitchTask task })
{
return;
}
@@ -227,7 +225,7 @@ private void btnTaskError_Click(object sender, RoutedEventArgs e)
private void btnRemoveTask_Click(object sender, RoutedEventArgs e)
{
- if (!(sender is Button { DataContext: ITwitchTask task }))
+ if (sender is not Button { DataContext: ITwitchTask task })
{
return;
}
diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
index 333fdbef..01cd107a 100644
--- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
@@ -49,8 +49,8 @@ private void SetEnabled(bool isEnabled)
checkEnd.IsEnabled = isEnabled;
SplitBtnDownload.IsEnabled = isEnabled;
MenuItemEnqueue.IsEnabled = isEnabled;
- SetEnabledCropStart(isEnabled & (bool)checkStart.IsChecked);
- SetEnabledCropEnd(isEnabled & (bool)checkEnd.IsChecked);
+ SetEnabledCropStart(isEnabled & checkStart.IsChecked.GetValueOrDefault());
+ SetEnabledCropEnd(isEnabled & checkEnd.IsChecked.GetValueOrDefault());
}
private void SetEnabledCropStart(bool isEnabled)
@@ -100,20 +100,14 @@ private async Task GetVideoInfo()
}
Task taskPlaylist = TwitchHelper.GetVideoPlaylist(videoId, taskAccessToken.Result.data.videoPlaybackAccessToken.value, taskAccessToken.Result.data.videoPlaybackAccessToken.signature);
- try
- {
- string thumbUrl = taskVideoInfo.Result.data.video.thumbnailURLs.FirstOrDefault();
- imgThumbnail.Source = await ThumbnailService.GetThumb(thumbUrl);
- }
- catch
+
+ var thumbUrl = taskVideoInfo.Result.data.video.thumbnailURLs.FirstOrDefault();
+ if (!ThumbnailService.TryGetThumb(thumbUrl, out var image))
{
AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
- var (success, image) = await ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL);
- if (success)
- {
- imgThumbnail.Source = image;
- }
+ _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out image);
}
+ imgThumbnail.Source = image;
comboQuality.Items.Clear();
videoQualities.Clear();
@@ -217,9 +211,9 @@ public VideoDownloadOptions GetOptions(string filename, string folder)
Oauth = TextOauth.Text,
Quality = GetQualityWithoutSize(comboQuality.Text).ToString(),
Id = currentVideoId,
- CropBeginning = (bool)checkStart.IsChecked,
+ CropBeginning = checkStart.IsChecked.GetValueOrDefault(),
CropBeginningTime = (int)(new TimeSpan((int)numStartHour.Value, (int)numStartMinute.Value, (int)numStartSecond.Value).TotalSeconds),
- CropEnding = (bool)checkEnd.IsChecked,
+ CropEnding = checkEnd.IsChecked.GetValueOrDefault(),
CropEndingTime = (int)(new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value).TotalSeconds),
FfmpegPath = "ffmpeg",
TempFolder = Settings.Default.TempPath
@@ -306,7 +300,7 @@ private static int ValidateUrl(string text)
public bool ValidateInputs()
{
- if ((bool)checkStart.IsChecked)
+ if (checkStart.IsChecked.GetValueOrDefault())
{
var beginTime = new TimeSpan((int)numStartHour.Value, (int)numStartMinute.Value, (int)numStartSecond.Value);
if (beginTime.TotalSeconds >= vodLength.TotalSeconds)
@@ -314,7 +308,7 @@ public bool ValidateInputs()
return false;
}
- if ((bool)checkEnd.IsChecked)
+ if (checkEnd.IsChecked.GetValueOrDefault())
{
var endTime = new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value);
if (endTime.TotalSeconds < beginTime.TotalSeconds)
@@ -381,14 +375,14 @@ private void Page_Loaded(object sender, RoutedEventArgs e)
private void checkStart_OnCheckStateChanged(object sender, RoutedEventArgs e)
{
- SetEnabledCropStart((bool)checkStart.IsChecked);
+ SetEnabledCropStart(checkStart.IsChecked.GetValueOrDefault());
UpdateVideoSizeEstimates();
}
private void checkEnd_OnCheckStateChanged(object sender, RoutedEventArgs e)
{
- SetEnabledCropEnd((bool)checkEnd.IsChecked);
+ SetEnabledCropEnd(checkEnd.IsChecked.GetValueOrDefault());
UpdateVideoSizeEstimates();
}
diff --git a/TwitchDownloaderWPF/Services/ThumbnailService.cs b/TwitchDownloaderWPF/Services/ThumbnailService.cs
index 102b4445..84dc969a 100644
--- a/TwitchDownloaderWPF/Services/ThumbnailService.cs
+++ b/TwitchDownloaderWPF/Services/ThumbnailService.cs
@@ -1,5 +1,4 @@
-using System.Net.Http;
-using System.Threading.Tasks;
+using System;
using System.Windows.Media.Imaging;
namespace TwitchDownloaderWPF.Services
@@ -7,33 +6,42 @@ namespace TwitchDownloaderWPF.Services
public static class ThumbnailService
{
public const string THUMBNAIL_MISSING_URL = @"https://vod-secure.twitch.tv/_404/404_processing_320x180.png";
- private static readonly HttpClient HttpClient = new();
- public static async Task GetThumb(string thumbUrl)
+ public static BitmapImage GetThumb(string thumbUrl, BitmapCacheOption cacheOption = BitmapCacheOption.OnLoad)
{
- if (string.IsNullOrWhiteSpace(thumbUrl))
- {
- return null;
- }
+ ArgumentNullException.ThrowIfNull(thumbUrl);
- BitmapImage img = new BitmapImage();
- img.CacheOption = BitmapCacheOption.OnLoad;
+ var img = new BitmapImage { CacheOption = cacheOption };
img.BeginInit();
- img.StreamSource = await HttpClient.GetStreamAsync(thumbUrl);
+ img.UriSource = new Uri(thumbUrl);
img.EndInit();
+ img.DownloadCompleted += static (sender, _) =>
+ {
+ if (sender is BitmapImage { CanFreeze: true } image)
+ {
+ image.Freeze();
+ }
+ };
return img;
}
- public static async Task<(bool success, BitmapImage image)> TryGetThumb(string thumbUrl)
+ public static bool TryGetThumb(string thumbUrl, out BitmapImage thumbnail)
{
+ if (string.IsNullOrWhiteSpace(thumbUrl))
+ {
+ thumbnail = null;
+ return false;
+ }
+
try
{
- var thumb = await GetThumb(thumbUrl);
- return (thumb != null, thumb);
+ thumbnail = GetThumb(thumbUrl);
+ return thumbnail != null;
}
catch
{
- return (false, null);
+ thumbnail = null;
+ return false;
}
}
}
diff --git a/TwitchDownloaderWPF/WindowMassDownload.xaml b/TwitchDownloaderWPF/WindowMassDownload.xaml
index 0d84b420..3167d84c 100644
--- a/TwitchDownloaderWPF/WindowMassDownload.xaml
+++ b/TwitchDownloaderWPF/WindowMassDownload.xaml
@@ -4,12 +4,14 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TwitchDownloaderWPF"
+ xmlns:task="clr-namespace:TwitchDownloaderWPF.TwitchTasks"
xmlns:behave="clr-namespace:TwitchDownloaderWPF.Behaviors"
xmlns:lex="http://wpflocalizeextension.codeplex.com"
lex:LocalizeDictionary.DesignCulture=""
lex:ResxLocalizationProvider.DefaultAssembly="TwitchDownloaderWPF"
lex:ResxLocalizationProvider.DefaultDictionary="Strings"
- xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
+ xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
+ xmlns:gif="http://wpfanimatedgif.codeplex.com"
mc:Ignorable="d"
Title="Mass Downloader" MinHeight="250" Height="700" MinWidth="775" Width="1100" Loaded="Window_Loaded">
@@ -19,22 +21,24 @@
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
-
-
+
@@ -63,14 +67,14 @@
-
-
(?):
-
+
diff --git a/TwitchDownloaderWPF/WindowSettings.xaml.cs b/TwitchDownloaderWPF/WindowSettings.xaml.cs
index 3ab1ac26..7eb419ac 100644
--- a/TwitchDownloaderWPF/WindowSettings.xaml.cs
+++ b/TwitchDownloaderWPF/WindowSettings.xaml.cs
@@ -119,11 +119,11 @@ private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs
Settings.Default.TemplateClip = textClipTemplate.Text;
Settings.Default.TemplateChat = textChatTemplate.Text;
Settings.Default.TempPath = textTempPath.Text;
- Settings.Default.HideDonation = (bool)checkDonation.IsChecked;
- Settings.Default.VerboseErrors = (bool)checkVerboseErrors.IsChecked;
+ Settings.Default.HideDonation = checkDonation.IsChecked.GetValueOrDefault();
+ Settings.Default.VerboseErrors = checkVerboseErrors.IsChecked.GetValueOrDefault();
Settings.Default.MaximumBandwidthKib = (int)NumMaximumBandwidth.Value;
- Settings.Default.UTCVideoTime = (bool)radioTimeFormatUTC.IsChecked;
- Settings.Default.DownloadThrottleEnabled = (bool)CheckThrottleEnabled.IsChecked;
+ Settings.Default.UTCVideoTime = radioTimeFormatUTC.IsChecked.GetValueOrDefault();
+ Settings.Default.DownloadThrottleEnabled = CheckThrottleEnabled.IsChecked.GetValueOrDefault();
Settings.Default.Save();
}
@@ -165,14 +165,9 @@ private void comboLocale_SelectionChanged(object sender, SelectionChangedEventAr
}
}
- private void CheckThrottleEnabled_Checked(object sender, RoutedEventArgs e)
+ private void CheckThrottleEnabled_CheckedChanged(object sender, RoutedEventArgs e)
{
- NumMaximumBandwidth.IsEnabled = true;
- }
-
- private void CheckThrottleEnabled_Unchecked(object sender, RoutedEventArgs e)
- {
- NumMaximumBandwidth.IsEnabled = false;
+ NumMaximumBandwidth.IsEnabled = CheckThrottleEnabled.IsChecked.GetValueOrDefault();
}
}
}
diff --git a/TwitchDownloaderWPF/WindowUrlList.xaml.cs b/TwitchDownloaderWPF/WindowUrlList.xaml.cs
index 44821370..714c6e0e 100644
--- a/TwitchDownloaderWPF/WindowUrlList.xaml.cs
+++ b/TwitchDownloaderWPF/WindowUrlList.xaml.cs
@@ -3,10 +3,10 @@
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
-using System.Windows.Media.Imaging;
using TwitchDownloaderCore;
using TwitchDownloaderCore.TwitchObjects.Gql;
using TwitchDownloaderWPF.Properties;
+using TwitchDownloaderWPF.Services;
using TwitchDownloaderWPF.TwitchTasks;
namespace TwitchDownloaderWPF
@@ -58,7 +58,7 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e)
foreach (var id in idList)
{
- if (id.All(Char.IsDigit))
+ if (id.All(char.IsDigit))
{
Task task = TwitchHelper.GetVideoInfo(int.Parse(id));
taskVideoList.Add(task);
@@ -77,73 +77,76 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e)
await Task.WhenAll(taskVideoList.ToArray());
}
catch { }
- try
- {
- await Task.WhenAll(taskClipList.ToArray());
- }
- catch { }
- for (int i = 0; i < taskVideoList.Count; i++)
+ foreach (var task in taskVideoList)
{
- if (taskVideoList[i].IsCompleted)
+ if (!task.IsCompleted)
+ continue;
+
+ string id = taskDict[task.Id];
+ if (!task.IsFaulted)
{
- string id = taskDict[taskVideoList[i].Id];
- if (!taskVideoList[i].IsFaulted)
+ var videoInfo = task.Result.data.video;
+ var thumbUrl = videoInfo.thumbnailURLs.FirstOrDefault();
+ if (!ThumbnailService.TryGetThumb(thumbUrl, out var thumbnail))
{
- GqlVideoResponse data = taskVideoList[i].Result;
- TaskData newData = new TaskData();
- newData.Id = id;
- try
- {
- string thumbUrl = data.data.video.thumbnailURLs.FirstOrDefault();
- var bitmapImage = new BitmapImage();
- bitmapImage.BeginInit();
- bitmapImage.UriSource = new Uri(thumbUrl);
- bitmapImage.EndInit();
- newData.Thumbnail = bitmapImage;
- }
- catch { }
- newData.Title = data.data.video.title;
- newData.Streamer = data.data.video.owner.displayName;
- newData.Time = Settings.Default.UTCVideoTime ? data.data.video.createdAt : data.data.video.createdAt.ToLocalTime();
- dataList.Add(newData);
+ _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out thumbnail);
}
- else
+
+ dataList.Add(new TaskData
{
- errorList.Add(idDict[id]);
- }
+ Id = id,
+ Thumbnail = thumbnail,
+ Title = videoInfo.title,
+ Streamer = videoInfo.owner.displayName,
+ Time = Settings.Default.UTCVideoTime ? videoInfo.createdAt : videoInfo.createdAt.ToLocalTime(),
+ Views = videoInfo.viewCount,
+ Game = videoInfo.game?.displayName ?? "Unknown",
+ Length = videoInfo.lengthSeconds
+ });
+ }
+ else
+ {
+ errorList.Add(idDict[id]);
}
}
- for (int i = 0; i < taskClipList.Count; i++)
+ try
{
- if (taskClipList[i].IsCompleted)
+ await Task.WhenAll(taskClipList.ToArray());
+ }
+ catch { }
+
+ foreach (var task in taskClipList)
+ {
+ if (!task.IsCompleted)
+ continue;
+
+ string id = taskDict[task.Id];
+ if (!task.IsFaulted)
{
- string id = taskDict[taskClipList[i].Id];
- if (!taskClipList[i].IsFaulted)
+ var clipInfo = task.Result.data.clip;
+ var thumbUrl = clipInfo.thumbnailURL;
+ if (!ThumbnailService.TryGetThumb(thumbUrl, out var thumbnail))
{
- GqlClipResponse data = taskClipList[i].Result;
- TaskData newData = new TaskData();
- newData.Id = id;
- try
- {
- string thumbUrl = data.data.clip.thumbnailURL;
- var bitmapImage = new BitmapImage();
- bitmapImage.BeginInit();
- bitmapImage.UriSource = new Uri(thumbUrl);
- bitmapImage.EndInit();
- newData.Thumbnail = bitmapImage;
- }
- catch { }
- newData.Title = data.data.clip.title;
- newData.Streamer = data.data.clip.broadcaster.displayName;
- newData.Time = Settings.Default.UTCVideoTime ? data.data.clip.createdAt : data.data.clip.createdAt.ToLocalTime();
- dataList.Add(newData);
+ _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out thumbnail);
}
- else
+
+ dataList.Add(new TaskData
{
- errorList.Add(idDict[id]);
- }
+ Id = id,
+ Thumbnail = thumbnail,
+ Title = clipInfo.title,
+ Streamer = clipInfo.broadcaster.displayName,
+ Time = Settings.Default.UTCVideoTime ? clipInfo.createdAt : clipInfo.createdAt.ToLocalTime(),
+ Views = clipInfo.viewCount,
+ Game = clipInfo.game?.displayName ?? "Unknown",
+ Length = clipInfo.durationSeconds
+ });
+ }
+ else
+ {
+ errorList.Add(idDict[id]);
}
}
@@ -154,8 +157,7 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e)
}
WindowQueueOptions queue = new WindowQueueOptions(dataList);
- bool? queued = queue.ShowDialog();
- if (queued != null && (bool)queued)
+ if (queue.ShowDialog().GetValueOrDefault())
this.Close();
btnQueue.IsEnabled = true;
From e32f5ce2c7aa2e49a91608b164ae6b6be6bdd023 Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Thu, 5 Oct 2023 21:38:07 -0400
Subject: [PATCH 27/50] Add videos per page selector to mass downloaders (#837)
* Add "videos per page" selector to mass downloader windows
* Update translations
* Remove unused imports
---
TwitchDownloaderWPF/PageQueue.xaml | 1 -
.../Translations/Strings.Designer.cs | 9 +++++
.../Translations/Strings.es.resx | 3 ++
.../Translations/Strings.fr.resx | 3 ++
.../Translations/Strings.pl.resx | 3 ++
TwitchDownloaderWPF/Translations/Strings.resx | 3 ++
.../Translations/Strings.ru.resx | 3 ++
.../Translations/Strings.tr.resx | 3 ++
.../Translations/Strings.zh.resx | 3 ++
TwitchDownloaderWPF/WindowMassDownload.xaml | 16 ++++++---
.../WindowMassDownload.xaml.cs | 35 +++++++++++++++----
11 files changed, 69 insertions(+), 13 deletions(-)
diff --git a/TwitchDownloaderWPF/PageQueue.xaml b/TwitchDownloaderWPF/PageQueue.xaml
index 90a00e73..cfe0dca9 100644
--- a/TwitchDownloaderWPF/PageQueue.xaml
+++ b/TwitchDownloaderWPF/PageQueue.xaml
@@ -4,7 +4,6 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TwitchDownloaderWPF"
- xmlns:task="clr-namespace:TwitchDownloaderWPF.TwitchTasks"
xmlns:lex="http://wpflocalizeextension.codeplex.com"
lex:LocalizeDictionary.DesignCulture=""
lex:ResxLocalizationProvider.DefaultAssembly="TwitchDownloaderWPF"
diff --git a/TwitchDownloaderWPF/Translations/Strings.Designer.cs b/TwitchDownloaderWPF/Translations/Strings.Designer.cs
index 36a31fcb..8a438986 100644
--- a/TwitchDownloaderWPF/Translations/Strings.Designer.cs
+++ b/TwitchDownloaderWPF/Translations/Strings.Designer.cs
@@ -941,6 +941,15 @@ public static string JsonFile {
}
}
+ ///
+ /// Looks up a localized string similar to Videos per page:.
+ ///
+ public static string LabelVideosPerPage {
+ get {
+ return ResourceManager.GetString("LabelVideosPerPage", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Length:.
///
diff --git a/TwitchDownloaderWPF/Translations/Strings.es.resx b/TwitchDownloaderWPF/Translations/Strings.es.resx
index 9a010065..aac64f8b 100644
--- a/TwitchDownloaderWPF/Translations/Strings.es.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.es.resx
@@ -772,4 +772,7 @@
Unable to start Windows application theme watcher. Error code: {0}
+
+ Videos per page:
+
diff --git a/TwitchDownloaderWPF/Translations/Strings.fr.resx b/TwitchDownloaderWPF/Translations/Strings.fr.resx
index 0fbdff6b..f3de78f0 100644
--- a/TwitchDownloaderWPF/Translations/Strings.fr.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.fr.resx
@@ -771,4 +771,7 @@
Impossible de démarrer l'observateur de thème de l'application Windows. Code d'erreur : {0}
+
+ Vidéos par page:
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.pl.resx b/TwitchDownloaderWPF/Translations/Strings.pl.resx
index 7fc6c681..d7f6e125 100644
--- a/TwitchDownloaderWPF/Translations/Strings.pl.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.pl.resx
@@ -771,4 +771,7 @@
Unable to start Windows application theme watcher. Error code: {0}
+
+ Videos per page:
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.resx b/TwitchDownloaderWPF/Translations/Strings.resx
index 112829f2..4ed524b0 100644
--- a/TwitchDownloaderWPF/Translations/Strings.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.resx
@@ -770,4 +770,7 @@
Unable to start Windows application theme watcher. Error code: {0}
+
+ Videos per page:
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.ru.resx b/TwitchDownloaderWPF/Translations/Strings.ru.resx
index 06fb8a90..7e77b56a 100644
--- a/TwitchDownloaderWPF/Translations/Strings.ru.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.ru.resx
@@ -771,4 +771,7 @@
Unable to start Windows application theme watcher. Error code: {0}
+
+ Videos per page:
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.tr.resx b/TwitchDownloaderWPF/Translations/Strings.tr.resx
index 4b1240e1..4bed4d11 100644
--- a/TwitchDownloaderWPF/Translations/Strings.tr.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.tr.resx
@@ -772,4 +772,7 @@
Unable to start Windows application theme watcher. Error code: {0}
+
+ Videos per page:
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.zh.resx b/TwitchDownloaderWPF/Translations/Strings.zh.resx
index b9d0cad1..a6c708f5 100644
--- a/TwitchDownloaderWPF/Translations/Strings.zh.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.zh.resx
@@ -770,4 +770,7 @@
Unable to start Windows application theme watcher. Error code: {0}
+
+ Videos per page:
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/WindowMassDownload.xaml b/TwitchDownloaderWPF/WindowMassDownload.xaml
index 3167d84c..8be36e6b 100644
--- a/TwitchDownloaderWPF/WindowMassDownload.xaml
+++ b/TwitchDownloaderWPF/WindowMassDownload.xaml
@@ -4,7 +4,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TwitchDownloaderWPF"
- xmlns:task="clr-namespace:TwitchDownloaderWPF.TwitchTasks"
xmlns:behave="clr-namespace:TwitchDownloaderWPF.Behaviors"
xmlns:lex="http://wpflocalizeextension.codeplex.com"
lex:LocalizeDictionary.DesignCulture=""
@@ -22,8 +21,8 @@
-
-
+
+
@@ -31,11 +30,18 @@
-
+
+
-
+
+
+
+
+
+
+
diff --git a/TwitchDownloaderWPF/WindowMassDownload.xaml.cs b/TwitchDownloaderWPF/WindowMassDownload.xaml.cs
index 70bfbb54..02424b58 100644
--- a/TwitchDownloaderWPF/WindowMassDownload.xaml.cs
+++ b/TwitchDownloaderWPF/WindowMassDownload.xaml.cs
@@ -27,6 +27,7 @@ public partial class WindowMassDownload : Window
public int cursorIndex = -1;
public string currentChannel = "";
public string period = "";
+ public int videoCount = 50;
public WindowMassDownload(DownloadType type)
{
@@ -35,8 +36,8 @@ public WindowMassDownload(DownloadType type)
itemList.ItemsSource = videoList;
if (downloaderType == DownloadType.Video)
{
- comboSort.Visibility = Visibility.Hidden;
- labelSort.Visibility = Visibility.Hidden;
+ ComboSortByDate.Visibility = Visibility.Hidden;
+ LabelSort.Visibility = Visibility.Hidden;
}
btnNext.IsEnabled = false;
btnPrev.IsEnabled = false;
@@ -60,6 +61,17 @@ private async Task UpdateList()
{
if (StatusImage != null) StatusImage.Visibility = Visibility.Visible;
+ if (string.IsNullOrWhiteSpace(currentChannel))
+ {
+ // Pretend we are doing something so the status icon has time to show
+ await Task.Delay(50);
+ videoList.Clear();
+ cursorList.Clear();
+ cursorIndex = -1;
+ if (StatusImage != null) StatusImage.Visibility = Visibility.Hidden;
+ return;
+ }
+
if (downloaderType == DownloadType.Video)
{
string currentCursor = "";
@@ -67,7 +79,7 @@ private async Task UpdateList()
{
currentCursor = cursorList[cursorIndex];
}
- GqlVideoSearchResponse res = await TwitchHelper.GetGqlVideos(currentChannel, currentCursor, 50);
+ GqlVideoSearchResponse res = await TwitchHelper.GetGqlVideos(currentChannel, currentCursor, videoCount);
videoList.Clear();
if (res.data.user != null)
{
@@ -109,7 +121,7 @@ private async Task UpdateList()
{
currentCursor = cursorList[cursorIndex];
}
- GqlClipSearchResponse res = await TwitchHelper.GetGqlClips(currentChannel, period, currentCursor, 50);
+ GqlClipSearchResponse res = await TwitchHelper.GetGqlClips(currentChannel, period, currentCursor, videoCount);
videoList.Clear();
if (res.data.user != null)
{
@@ -145,7 +157,7 @@ private async Task UpdateList()
}
}
- if (StatusImage != null) StatusImage.Visibility = Visibility.Collapsed;
+ if (StatusImage != null) StatusImage.Visibility = Visibility.Hidden;
}
private void Border_MouseUp(object sender, MouseButtonEventArgs e)
@@ -203,9 +215,9 @@ private void btnQueue_Click(object sender, RoutedEventArgs e)
}
}
- private async void comboSort_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ private async void ComboSortByDate_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
- period = ((ComboBoxItem)comboSort.SelectedItem).Tag.ToString();
+ period = ((ComboBoxItem)ComboSortByDate.SelectedItem).Tag.ToString();
videoList.Clear();
cursorList.Clear();
cursorIndex = -1;
@@ -250,5 +262,14 @@ private async void TextChannel_OnKeyDown(object sender, KeyEventArgs e)
e.Handled = true;
}
}
+
+ private async void ComboVideoCount_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ videoCount = int.Parse((string)((ComboBoxItem)ComboVideoCount.SelectedValue).Content);
+ videoList.Clear();
+ cursorList.Clear();
+ cursorIndex = -1;
+ await UpdateList();
+ }
}
}
From f9d98578c9830fc46dd17f6b3d1d45af4d031fd8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mahmut=20S=C3=B6zen?=
<37031361+Teknoist@users.noreply.github.com>
Date: Fri, 6 Oct 2023 06:19:02 +0300
Subject: [PATCH 28/50] Update Strings.tr.resx (#836)
Co-authored-by: Scrub <72096833+ScrubN@users.noreply.github.com>
---
.../Translations/Strings.tr.resx | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/TwitchDownloaderWPF/Translations/Strings.tr.resx b/TwitchDownloaderWPF/Translations/Strings.tr.resx
index 4bed4d11..08b595aa 100644
--- a/TwitchDownloaderWPF/Translations/Strings.tr.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.tr.resx
@@ -740,37 +740,37 @@
C# standart TimeSpan biçim dizeleri
- An unknown error occurred
+ Bilinmeyen bir hata oluştu
- The task could not be removed
+ Görev kaldırılamadı
- Please cancel the task or wait for it to finish before removing it
+ Lütfen görevi iptal edin veya kaldırmadan önce bitmesini bekleyin
- Unable to download FFmpeg
+ FFmpeg indirilemiyor
- Unable to download FFmpeg. Please manually download it from {0} and place the file at {1}
+ FFmpeg indirilemiyor. Lütfen {0} adresinden manuel olarak indirin ve dosyayı {1} konumuna yerleştirin.
- Alt Background Color:
+ Alternatif Arka Plan Rengi:
Alternatif Arka Planlar Leave a trailing space
- Alternates the background color of every other chat message to help tell them apart.
+ Diğer sohbet mesajlarının arka plan rengini değiştirerek onları birbirinden ayırmaya yardımcı olur.
- Encode Metadata:
+ Meta Verileri Kodlayın:
Hata
- Unable to start Windows application theme watcher. Error code: {0}
+ Windows uygulama teması izleyicisi başlatılamıyor. Hata kodu: {0}
Videos per page:
From 904186be5a2e85e3c6bd90d17665db1039e977d4 Mon Sep 17 00:00:00 2001
From: DeciBelioS <96150975+Deci8BelioS@users.noreply.github.com>
Date: Fri, 6 Oct 2023 05:19:14 +0200
Subject: [PATCH 29/50] update spanish translation (#801)
* Update Strings.es.resx
* Create Readme_es.md
* Rename Readme_es.md to README_es.md
* Update README.md
* Update Strings.es.resx
---
TwitchDownloaderWPF/Translations/Strings.es.resx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/TwitchDownloaderWPF/Translations/Strings.es.resx b/TwitchDownloaderWPF/Translations/Strings.es.resx
index aac64f8b..35030a92 100644
--- a/TwitchDownloaderWPF/Translations/Strings.es.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.es.resx
@@ -770,7 +770,7 @@
Error
- Unable to start Windows application theme watcher. Error code: {0}
+ No se puede iniciar el observador de temas de la aplicación de Windows. Código de error: {0}
Videos per page:
From 870c667d526be97080b53f1ab5db2718a0183e11 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Thu, 5 Oct 2023 23:28:05 -0400
Subject: [PATCH 30/50] Add some NotNullWhen attributes
---
TwitchDownloaderCore/ChatRenderer.cs | 13 +++++++------
TwitchDownloaderWPF/Services/ThumbnailService.cs | 4 +++-
2 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/TwitchDownloaderCore/ChatRenderer.cs b/TwitchDownloaderCore/ChatRenderer.cs
index 78fc1265..4059f8b4 100644
--- a/TwitchDownloaderCore/ChatRenderer.cs
+++ b/TwitchDownloaderCore/ChatRenderer.cs
@@ -5,6 +5,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -814,7 +815,7 @@ private void DrawFragmentPart(List<(SKImageInfo info, SKBitmap bitmap)> sectionI
DrawRegularMessage(sectionImages, emotePositionList, ref drawPos, defaultPos, bitsCount, fragmentPart, highlightWords);
}
- static bool TryGetTwitchEmote(List twitchEmoteList, ReadOnlySpan emoteName, out TwitchEmote twitchEmote)
+ static bool TryGetTwitchEmote(List twitchEmoteList, ReadOnlySpan emoteName, [NotNullWhen(true)] out TwitchEmote twitchEmote)
{
// Enumerating over a span is faster than a list
var emoteListSpan = CollectionsMarshal.AsSpan(twitchEmoteList);
@@ -827,7 +828,7 @@ static bool TryGetTwitchEmote(List twitchEmoteList, ReadOnlySpan sectio
DrawText(fragmentString, messageFont, true, sectionImages, ref drawPos, defaultPos, highlightWords);
}
- static bool TryGetCheerEmote(List cheerEmoteList, ReadOnlySpan prefix, out CheerEmote cheerEmote)
+ static bool TryGetCheerEmote(List cheerEmoteList, ReadOnlySpan prefix, [NotNullWhen(true)] out CheerEmote cheerEmote)
{
// Enumerating over a span is faster than a list
var cheerEmoteListSpan = CollectionsMarshal.AsSpan(cheerEmoteList);
@@ -1089,7 +1090,7 @@ static bool TryGetCheerEmote(List cheerEmoteList, ReadOnlySpan
}
}
- cheerEmote = default;
+ cheerEmote = null;
return false;
}
}
@@ -1125,7 +1126,7 @@ private void DrawFirstPartyEmote(List<(SKImageInfo info, SKBitmap bitmap)> secti
DrawText(fragment.text, messageFont, true, sectionImages, ref drawPos, defaultPos, highlightWords);
}
- static bool TryGetTwitchEmote(List twitchEmoteList, ReadOnlySpan emoteId, out TwitchEmote twitchEmote)
+ static bool TryGetTwitchEmote(List twitchEmoteList, ReadOnlySpan emoteId, [NotNullWhen(true)] out TwitchEmote twitchEmote)
{
// Enumerating over a span is faster than a list
var emoteListSpan = CollectionsMarshal.AsSpan(twitchEmoteList);
@@ -1138,7 +1139,7 @@ static bool TryGetTwitchEmote(List twitchEmoteList, ReadOnlySpanThe was
public static BitmapImage GetThumb(string thumbUrl, BitmapCacheOption cacheOption = BitmapCacheOption.OnLoad)
{
ArgumentNullException.ThrowIfNull(thumbUrl);
@@ -25,7 +27,7 @@ public static BitmapImage GetThumb(string thumbUrl, BitmapCacheOption cacheOptio
return img;
}
- public static bool TryGetThumb(string thumbUrl, out BitmapImage thumbnail)
+ public static bool TryGetThumb(string thumbUrl, [NotNullWhen(true)] out BitmapImage thumbnail)
{
if (string.IsNullOrWhiteSpace(thumbUrl))
{
From 8d7982d5a53aef3e47f2df397f914cad8d444566 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Thu, 5 Oct 2023 23:32:03 -0400
Subject: [PATCH 31/50] Remove explicit copyright year
---
LICENSE.txt | 2 +-
TwitchDownloaderCLI/TwitchDownloaderCLI.csproj | 2 +-
TwitchDownloaderCore/Resources/TD-License | 2 +-
TwitchDownloaderWPF/Properties/AssemblyInfo.cs | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/LICENSE.txt b/LICENSE.txt
index 30832319..eaff5a43 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
The MIT License
-Copyright (c) 2019 lay295
+Copyright (c) lay295
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
index e9dd74df..8c2df6bd 100644
--- a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
+++ b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
@@ -3,7 +3,7 @@
Exe
1.53.2
- Copyright © 2019 lay295 and contributors
+ Copyright © lay295 and contributors
Download and render Twitch VODs, clips, and chats
MIT
AnyCPU;x64
diff --git a/TwitchDownloaderCore/Resources/TD-License b/TwitchDownloaderCore/Resources/TD-License
index 726d8e0e..88a32149 100644
--- a/TwitchDownloaderCore/Resources/TD-License
+++ b/TwitchDownloaderCore/Resources/TD-License
@@ -30,7 +30,7 @@ License
The MIT License
- Copyright (c) 2019 lay295 and contributors
+ Copyright (c) lay295
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/TwitchDownloaderWPF/Properties/AssemblyInfo.cs b/TwitchDownloaderWPF/Properties/AssemblyInfo.cs
index bade434f..2128e947 100644
--- a/TwitchDownloaderWPF/Properties/AssemblyInfo.cs
+++ b/TwitchDownloaderWPF/Properties/AssemblyInfo.cs
@@ -12,7 +12,7 @@
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("TwitchDownloader")]
-[assembly: AssemblyCopyright("Copyright © 2019 lay295 and contributors")]
+[assembly: AssemblyCopyright("Copyright © lay295 and contributors")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
From a7d3bc48353ae57fc59c54cfbe1aaaae574c9eab Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 6 Oct 2023 00:57:33 -0400
Subject: [PATCH 32/50] Better FFmpeg download progress reporting (#840)
* Add FFmpeg download progress reporting to WPF
* Update translations
* Move expensive console writing off of FFmpeg download thread.
---
TwitchDownloaderCLI/Tools/FfmpegHandler.cs | 35 ++++++++++++++---
TwitchDownloaderWPF/MainWindow.xaml.cs | 39 +++++++++++++++++--
.../Translations/Strings.Designer.cs | 9 +++++
.../Translations/Strings.es.resx | 3 ++
.../Translations/Strings.fr.resx | 3 ++
.../Translations/Strings.pl.resx | 3 ++
TwitchDownloaderWPF/Translations/Strings.resx | 3 ++
.../Translations/Strings.ru.resx | 3 ++
.../Translations/Strings.tr.resx | 3 ++
.../Translations/Strings.zh.resx | 3 ++
10 files changed, 95 insertions(+), 9 deletions(-)
diff --git a/TwitchDownloaderCLI/Tools/FfmpegHandler.cs b/TwitchDownloaderCLI/Tools/FfmpegHandler.cs
index 8684d8e6..7a4bd379 100644
--- a/TwitchDownloaderCLI/Tools/FfmpegHandler.cs
+++ b/TwitchDownloaderCLI/Tools/FfmpegHandler.cs
@@ -1,7 +1,10 @@
using Mono.Unix;
using System;
+using System.Collections.Concurrent;
using System.IO;
+using System.Linq;
using System.Runtime.InteropServices;
+using System.Threading;
using TwitchDownloaderCLI.Modes.Arguments;
using Xabe.FFmpeg;
using Xabe.FFmpeg.Downloader;
@@ -24,8 +27,7 @@ private static void DownloadFfmpeg()
{
Console.Write("[INFO] - Downloading FFmpeg");
- var progressHandler = new Progress();
- progressHandler.ProgressChanged += new XabeProgressHandler().OnProgressReceived;
+ using var progressHandler = new XabeProgressHandler();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@@ -63,20 +65,41 @@ public static void DetectFfmpeg(string ffmpegPath)
Environment.Exit(1);
}
- private class XabeProgressHandler
+ private sealed class XabeProgressHandler : IProgress, IDisposable
{
private int _lastPercent = -1;
+ private readonly ConcurrentQueue _percentQueue = new();
+ private readonly Timer _timer;
- internal void OnProgressReceived(object sender, ProgressInfo e)
+ public XabeProgressHandler()
{
- var percent = (int)(e.DownloadedBytes / (double)e.TotalBytes * 100);
+ _timer = new Timer(Callback, _percentQueue, 0, 100);
+
+ static void Callback(object state)
+ {
+ if (state is not ConcurrentQueue { IsEmpty: false } queue) return;
+
+ var currentPercent = queue.Max();
+ Console.Write($"\r[INFO] - Downloading FFmpeg {currentPercent}%");
+ }
+ }
+
+ public void Report(ProgressInfo value)
+ {
+ var percent = (int)(value.DownloadedBytes / (double)value.TotalBytes * 100);
if (percent > _lastPercent)
{
_lastPercent = percent;
- Console.Write($"\r[INFO] - Downloading FFmpeg {percent}%");
+ _percentQueue.Enqueue(percent);
}
}
+
+ public void Dispose()
+ {
+ _timer?.Dispose();
+ _percentQueue.Clear();
+ }
}
}
}
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/MainWindow.xaml.cs b/TwitchDownloaderWPF/MainWindow.xaml.cs
index aaf42a58..46ff2267 100644
--- a/TwitchDownloaderWPF/MainWindow.xaml.cs
+++ b/TwitchDownloaderWPF/MainWindow.xaml.cs
@@ -5,6 +5,7 @@
using System.Net;
using System.Windows;
using TwitchDownloaderWPF.Properties;
+using Xabe.FFmpeg;
using Xabe.FFmpeg.Downloader;
namespace TwitchDownloaderWPF
@@ -69,11 +70,16 @@ private async void Window_Loaded(object sender, RoutedEventArgs e)
Settings.Default.Save();
}
+ var currentVersion = Version.Parse("1.53.2");
+ Title = $"Twitch Downloader v{currentVersion}";
+
+ // TODO: extract FFmpeg handling to a dedicated service
if (!File.Exists("ffmpeg.exe"))
{
+ var oldTitle = Title;
try
{
- await FFmpegDownloader.GetLatestVersion(FFmpegVersion.Full);
+ await FFmpegDownloader.GetLatestVersion(FFmpegVersion.Full, new FfmpegDownloadProgress());
}
catch (Exception ex)
{
@@ -88,10 +94,10 @@ private async void Window_Loaded(object sender, RoutedEventArgs e)
MessageBox.Show(ex.ToString(), Translations.Strings.VerboseErrorOutput, MessageBoxButton.OK, MessageBoxImage.Error);
}
}
+
+ Title = oldTitle;
}
- Version currentVersion = new Version("1.53.2");
- Title = $"Twitch Downloader v{currentVersion}";
AutoUpdater.InstalledVersion = currentVersion;
#if !DEBUG
if (AppContext.BaseDirectory.StartsWith(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)))
@@ -102,5 +108,32 @@ private async void Window_Loaded(object sender, RoutedEventArgs e)
AutoUpdater.Start("https://downloader-update.twitcharchives.workers.dev");
#endif
}
+
+ private class FfmpegDownloadProgress : IProgress
+ {
+ private int _lastPercent = -1;
+
+ public void Report(ProgressInfo value)
+ {
+ var percent = (int)(value.DownloadedBytes / (double)value.TotalBytes * 100);
+
+ if (percent > _lastPercent)
+ {
+ var window = Application.Current.MainWindow;
+ if (window is null) return;
+
+ _lastPercent = percent;
+
+ var oldTitle = window.Title;
+ if (oldTitle.IndexOf('-') == -1) oldTitle += " -";
+
+ window.Title = string.Concat(
+ oldTitle.AsSpan(0, oldTitle.IndexOf('-')),
+ "- ",
+ string.Format(Translations.Strings.StatusDownloaderFFmpeg, percent.ToString())
+ );
+ }
+ }
+ }
}
}
diff --git a/TwitchDownloaderWPF/Translations/Strings.Designer.cs b/TwitchDownloaderWPF/Translations/Strings.Designer.cs
index 8a438986..0ddc9a35 100644
--- a/TwitchDownloaderWPF/Translations/Strings.Designer.cs
+++ b/TwitchDownloaderWPF/Translations/Strings.Designer.cs
@@ -1382,6 +1382,15 @@ public static string StatusDone {
}
}
+ ///
+ /// Looks up a localized string similar to Downloading FFmpeg {0}%.
+ ///
+ public static string StatusDownloaderFFmpeg {
+ get {
+ return ResourceManager.GetString("StatusDownloaderFFmpeg", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Downloading.
///
diff --git a/TwitchDownloaderWPF/Translations/Strings.es.resx b/TwitchDownloaderWPF/Translations/Strings.es.resx
index 35030a92..4228ce6b 100644
--- a/TwitchDownloaderWPF/Translations/Strings.es.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.es.resx
@@ -775,4 +775,7 @@
Videos per page:
+
+ Downloading FFmpeg {0}%
+
diff --git a/TwitchDownloaderWPF/Translations/Strings.fr.resx b/TwitchDownloaderWPF/Translations/Strings.fr.resx
index f3de78f0..f7a00758 100644
--- a/TwitchDownloaderWPF/Translations/Strings.fr.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.fr.resx
@@ -774,4 +774,7 @@
Vidéos par page:
+
+ Downloading FFmpeg {0}%
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.pl.resx b/TwitchDownloaderWPF/Translations/Strings.pl.resx
index d7f6e125..f9cb43b9 100644
--- a/TwitchDownloaderWPF/Translations/Strings.pl.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.pl.resx
@@ -774,4 +774,7 @@
Videos per page:
+
+ Downloading FFmpeg {0}%
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.resx b/TwitchDownloaderWPF/Translations/Strings.resx
index 4ed524b0..0bc72b6b 100644
--- a/TwitchDownloaderWPF/Translations/Strings.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.resx
@@ -773,4 +773,7 @@
Videos per page:
+
+ Downloading FFmpeg {0}%
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.ru.resx b/TwitchDownloaderWPF/Translations/Strings.ru.resx
index 7e77b56a..454118e6 100644
--- a/TwitchDownloaderWPF/Translations/Strings.ru.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.ru.resx
@@ -774,4 +774,7 @@
Videos per page:
+
+ Downloading FFmpeg {0}%
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.tr.resx b/TwitchDownloaderWPF/Translations/Strings.tr.resx
index 08b595aa..c830884c 100644
--- a/TwitchDownloaderWPF/Translations/Strings.tr.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.tr.resx
@@ -775,4 +775,7 @@
Videos per page:
+
+ Downloading FFmpeg {0}%
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.zh.resx b/TwitchDownloaderWPF/Translations/Strings.zh.resx
index a6c708f5..1073660f 100644
--- a/TwitchDownloaderWPF/Translations/Strings.zh.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.zh.resx
@@ -773,4 +773,7 @@
Videos per page:
+
+ Downloading FFmpeg {0}%
+
\ No newline at end of file
From 6bbc9266df1f4024e01227371bdd724f0b2722dd Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 6 Oct 2023 01:01:24 -0400
Subject: [PATCH 33/50] Bump version
---
TwitchDownloaderCLI/TwitchDownloaderCLI.csproj | 2 +-
TwitchDownloaderWPF/MainWindow.xaml.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
index 8c2df6bd..5b28edd1 100644
--- a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
+++ b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
@@ -2,7 +2,7 @@
Exe
- 1.53.2
+ 1.53.3
Copyright © lay295 and contributors
Download and render Twitch VODs, clips, and chats
MIT
diff --git a/TwitchDownloaderWPF/MainWindow.xaml.cs b/TwitchDownloaderWPF/MainWindow.xaml.cs
index 46ff2267..ba71e58b 100644
--- a/TwitchDownloaderWPF/MainWindow.xaml.cs
+++ b/TwitchDownloaderWPF/MainWindow.xaml.cs
@@ -70,7 +70,7 @@ private async void Window_Loaded(object sender, RoutedEventArgs e)
Settings.Default.Save();
}
- var currentVersion = Version.Parse("1.53.2");
+ var currentVersion = Version.Parse("1.53.3");
Title = $"Twitch Downloader v{currentVersion}";
// TODO: extract FFmpeg handling to a dedicated service
From 65344c67aa95ed7d0e0fadb4d2b66a5b821b1a0a Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 6 Oct 2023 16:33:58 -0400
Subject: [PATCH 34/50] Fix oversight I need to enable nullable ref types :/
---
TwitchDownloaderCore/ChatDownloader.cs | 2 +-
TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs | 2 +-
TwitchDownloaderCore/VideoDownloader.cs | 6 +++---
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/TwitchDownloaderCore/ChatDownloader.cs b/TwitchDownloaderCore/ChatDownloader.cs
index b6029c7a..5ae0da1d 100644
--- a/TwitchDownloaderCore/ChatDownloader.cs
+++ b/TwitchDownloaderCore/ChatDownloader.cs
@@ -279,7 +279,7 @@ public async Task DownloadAsync(IProgress progress, Cancellation
chatRoot.streamer.name = videoInfoResponse.data.video.owner.displayName;
chatRoot.streamer.id = int.Parse(videoInfoResponse.data.video.owner.id);
- chatRoot.video.description = videoInfoResponse.data.video.description.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd();
+ chatRoot.video.description = videoInfoResponse.data.video.description?.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd();
videoTitle = videoInfoResponse.data.video.title;
videoCreatedAt = videoInfoResponse.data.video.createdAt;
videoStart = downloadOptions.CropBeginning ? downloadOptions.CropBeginningTime : 0.0;
diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
index f6d1f0c4..986857c9 100644
--- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
+++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
@@ -26,7 +26,7 @@ public class VideoInfo
public VideoGame game { get; set; }
///
/// Some values, such as newlines, are repeated twice for some reason.
- /// This can be filtered out with: .Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd()
+ /// This can be filtered out with: description?.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd()
///
public string description { get; set; }
}
diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs
index eec2281c..0135dfad 100644
--- a/TwitchDownloaderCore/VideoDownloader.cs
+++ b/TwitchDownloaderCore/VideoDownloader.cs
@@ -98,7 +98,7 @@ public async Task DownloadAsync(CancellationToken cancellationToken)
string metadataPath = Path.Combine(downloadFolder, "metadata.txt");
VideoInfo videoInfo = videoInfoResponse.data.video;
await FfmpegMetadata.SerializeAsync(metadataPath, videoInfo.owner.displayName, downloadOptions.Id.ToString(), videoInfo.title, videoInfo.createdAt, videoInfo.viewCount,
- videoInfo.description.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd(), startOffset, videoChapterResponse.data.video.moments.edges, cancellationToken);
+ videoInfo.description?.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd(), startOffset, videoChapterResponse.data.video.moments.edges, cancellationToken);
var finalizedFileDirectory = Directory.GetParent(Path.GetFullPath(downloadOptions.Filename))!;
if (!finalizedFileDirectory.Exists)
@@ -223,8 +223,8 @@ static void ExecuteDownloadThread(object state)
throw;
}
- const int aPrimeNumber = 71;
- Thread.Sleep(aPrimeNumber);
+ const int A_PRIME_NUMBER = 71;
+ Thread.Sleep(A_PRIME_NUMBER);
}
}
From 8a4f904aff65f520a75f794aa87e99ad336c3cd3 Mon Sep 17 00:00:00 2001
From: DeciBelioS <96150975+Deci8BelioS@users.noreply.github.com>
Date: Fri, 6 Oct 2023 22:36:15 +0200
Subject: [PATCH 35/50] Spanish translation update (#842)
---
TwitchDownloaderWPF/Translations/Strings.es.resx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/TwitchDownloaderWPF/Translations/Strings.es.resx b/TwitchDownloaderWPF/Translations/Strings.es.resx
index 4228ce6b..6db5706a 100644
--- a/TwitchDownloaderWPF/Translations/Strings.es.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.es.resx
@@ -773,9 +773,9 @@
No se puede iniciar el observador de temas de la aplicación de Windows. Código de error: {0}
- Videos per page:
+ Vídeos por página:
- Downloading FFmpeg {0}%
+ Descarga FFmpeg {0}%
From 68a6b85c0d0766b59163fc6a84f3f8ccebfd56fc Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 6 Oct 2023 16:36:57 -0400
Subject: [PATCH 36/50] Bump version
---
TwitchDownloaderCLI/TwitchDownloaderCLI.csproj | 2 +-
TwitchDownloaderWPF/MainWindow.xaml.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
index 5b28edd1..d7b215cd 100644
--- a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
+++ b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
@@ -2,7 +2,7 @@
Exe
- 1.53.3
+ 1.53.4
Copyright © lay295 and contributors
Download and render Twitch VODs, clips, and chats
MIT
diff --git a/TwitchDownloaderWPF/MainWindow.xaml.cs b/TwitchDownloaderWPF/MainWindow.xaml.cs
index ba71e58b..31bacc86 100644
--- a/TwitchDownloaderWPF/MainWindow.xaml.cs
+++ b/TwitchDownloaderWPF/MainWindow.xaml.cs
@@ -70,7 +70,7 @@ private async void Window_Loaded(object sender, RoutedEventArgs e)
Settings.Default.Save();
}
- var currentVersion = Version.Parse("1.53.3");
+ var currentVersion = Version.Parse("1.53.4");
Title = $"Twitch Downloader v{currentVersion}";
// TODO: extract FFmpeg handling to a dedicated service
From fefd3d18f84185977b9959032dd423b7e1fed405 Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 6 Oct 2023 21:51:53 -0400
Subject: [PATCH 37/50] Fix old chats with stringified streamer ids being
undeserializable (#843)
---
TwitchDownloaderCore/Chat/ChatJson.cs | 16 +++++++++++++++-
TwitchDownloaderCore/TwitchObjects/ChatRoot.cs | 8 ++++++++
2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/TwitchDownloaderCore/Chat/ChatJson.cs b/TwitchDownloaderCore/Chat/ChatJson.cs
index 1eeb3f4f..6e04b41f 100644
--- a/TwitchDownloaderCore/Chat/ChatJson.cs
+++ b/TwitchDownloaderCore/Chat/ChatJson.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.IO.Compression;
using System.Linq;
+using System.Runtime.Serialization;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
@@ -66,7 +67,20 @@ public static class ChatJson
if (jsonDocument.RootElement.TryGetProperty("streamer", out JsonElement streamerElement))
{
- returnChatRoot.streamer = streamerElement.Deserialize(options: _jsonSerializerOptions);
+ if (returnChatRoot.FileInfo.Version > new ChatRootVersion(1, 0, 0))
+ {
+ returnChatRoot.streamer = streamerElement.Deserialize(options: _jsonSerializerOptions);
+ }
+ else
+ {
+ var legacyStreamer = streamerElement.Deserialize(options: _jsonSerializerOptions);
+ returnChatRoot.streamer = legacyStreamer.id.ValueKind switch
+ {
+ JsonValueKind.Number => new Streamer { name = legacyStreamer.name, id = legacyStreamer.id.GetInt32() },
+ JsonValueKind.String => new Streamer { name = legacyStreamer.name, id = int.Parse(legacyStreamer.id.GetString()!) },
+ _ => null // Fallback to UpgradeChatJson()
+ };
+ }
}
if (jsonDocument.RootElement.TryGetProperty("video", out JsonElement videoElement))
diff --git a/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs b/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs
index 36fa5b73..743add7c 100644
--- a/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs
+++ b/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Text.Json;
using System.Text.Json.Serialization;
namespace TwitchDownloaderCore.TwitchObjects
@@ -11,6 +12,13 @@ public class Streamer
public int id { get; set; }
}
+ public class LegacyStreamer
+ {
+ public string name { get; set; }
+ /// Some old chats use a string instead of an integer.
+ public JsonElement id { get; set; }
+ }
+
[DebuggerDisplay("{display_name}")]
public class Commenter
{
From 6d303243cf25b51dd9330c337e3b40baa5f4bfe4 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Tue, 10 Oct 2023 19:46:15 -0400
Subject: [PATCH 38/50] Simplify ChatRootVersion
---
.../TwitchObjects/ChatRootInfo.cs | 78 +++++--------------
1 file changed, 20 insertions(+), 58 deletions(-)
diff --git a/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs b/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs
index e4b60225..689de2a9 100644
--- a/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs
+++ b/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs
@@ -4,101 +4,63 @@ namespace TwitchDownloaderCore.TwitchObjects
{
public class ChatRootInfo
{
- public ChatRootVersion Version { get; init; } = new ChatRootVersion();
+ public ChatRootVersion Version { get; init; } = new();
public DateTime CreatedAt { get; init; } = DateTime.FromBinary(0);
public DateTime UpdatedAt { get; init; } = DateTime.FromBinary(0);
-
- public ChatRootInfo() { }
}
- public class ChatRootVersion
+ public record ChatRootVersion
{
- // Fields
- public int Major { get; set; } = 1;
- public int Minor { get; set; } = 0;
- public int Patch { get; set; } = 0;
+ public uint Major { get; init; }
+ public uint Minor { get; init; }
+ public uint Patch { get; init; }
public static ChatRootVersion CurrentVersion { get; } = new(1, 3, 1);
- // Constructors
///
/// Initializes a new object with the default version of 1.0.0
///
- public ChatRootVersion() { }
+ public ChatRootVersion()
+ {
+ Major = 1;
+ Minor = 0;
+ Patch = 0;
+ }
///
/// Initializes a new object with the version number of ..
///
- public ChatRootVersion(int major, int minor, int patch)
+ public ChatRootVersion(uint major, uint minor, uint patch)
{
Major = major;
Minor = minor;
Patch = patch;
}
- // Methods
public override string ToString()
=> $"{Major}.{Minor}.{Patch}";
public override int GetHashCode()
=> ToString().GetHashCode();
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0083:Use pattern matching")]
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(this, obj))
- return true;
-
- if (!(obj is ChatRootVersion crv))
- return false;
-
- return this == crv;
- }
-
- // Operators
public static bool operator >(ChatRootVersion left, ChatRootVersion right)
{
if (left.Major > right.Major) return true;
- else if (left.Major == right.Major)
- {
- if (left.Minor > right.Minor) return true;
- else if (left.Minor == right.Minor)
- {
- if (left.Patch > right.Patch) return true;
- }
- }
- return false;
- }
+ if (left.Major < right.Major) return false;
- public static bool operator <(ChatRootVersion left, ChatRootVersion right)
- {
- if (left.Major < right.Major) return true;
- else if (left.Major == right.Major)
- {
- if (left.Minor < right.Minor) return true;
- else if (left.Minor == right.Minor)
- {
- if (left.Patch < right.Patch) return true;
- }
- }
- return false;
- }
+ if (left.Minor > right.Minor) return true;
+ if (left.Minor < right.Minor) return false;
- public static bool operator ==(ChatRootVersion left, ChatRootVersion right)
- {
- if (left.Major != right.Major) return false;
- if (left.Minor != right.Minor) return false;
- if (left.Patch != right.Patch) return false;
- return true;
+ return left.Patch > right.Patch;
}
- public static bool operator !=(ChatRootVersion left, ChatRootVersion right)
- => !(left == right);
+ public static bool operator <(ChatRootVersion left, ChatRootVersion right)
+ => right > left;
public static bool operator >=(ChatRootVersion left, ChatRootVersion right)
- => (left > right) || (left == right);
+ => left == right || left > right;
public static bool operator <=(ChatRootVersion left, ChatRootVersion right)
- => (left < right) || (left == right);
+ => left == right || left < right;
}
}
From 3f7b79e8d54f3e8d31bcefe62c77924bf9485a8d Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Tue, 10 Oct 2023 19:50:14 -0400
Subject: [PATCH 39/50] Fix chats enqueued by the mass downloaders incorrectly
having `.json` file extension
---
TwitchDownloaderWPF/WindowQueueOptions.xaml.cs | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
index 1ccf950d..db63129b 100644
--- a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
+++ b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
@@ -252,7 +252,9 @@ private void btnQueue_Click(object sender, RoutedEventArgs e)
chatOptions.DownloadFormat = ChatFormat.Text;
chatOptions.TimeFormat = TimestampFormat.Relative;
chatOptions.EmbedData = checkEmbed.IsChecked.GetValueOrDefault();
- chatOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, downloadTask.Info.Title, chatOptions.Id, clipDownloadPage.currentVideoTime, clipDownloadPage.textStreamer.Text, TimeSpan.Zero, clipDownloadPage.clipLength, clipDownloadPage.viewCount.ToString(), clipDownloadPage.game) + "." + chatOptions.FileExtension);
+ chatOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, downloadTask.Info.Title, chatOptions.Id,
+ clipDownloadPage.currentVideoTime, clipDownloadPage.textStreamer.Text, TimeSpan.Zero, clipDownloadPage.clipLength,
+ clipDownloadPage.viewCount.ToString(), clipDownloadPage.game) + "." + chatOptions.FileExtension);
ChatDownloadTask chatTask = new ChatDownloadTask
{
@@ -313,7 +315,7 @@ private void btnQueue_Click(object sender, RoutedEventArgs e)
ChatDownloadOptions chatOptions = MainWindow.pageChatDownload.GetOptions(null);
chatOptions.Id = chatDownloadPage.downloadId;
- chatOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, chatDownloadPage.textTitle.Text, chatOptions.Id, chatDownloadPage.currentVideoTime, chatDownloadPage.textStreamer.Text,
+ chatOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, chatDownloadPage.textTitle.Text, chatOptions.Id,chatDownloadPage.currentVideoTime, chatDownloadPage.textStreamer.Text,
chatOptions.CropBeginning ? TimeSpan.FromSeconds(chatOptions.CropBeginningTime) : TimeSpan.Zero,
chatOptions.CropEnding ? TimeSpan.FromSeconds(chatOptions.CropEndingTime) : chatDownloadPage.vodLength,
chatDownloadPage.viewCount.ToString(), chatDownloadPage.game) + "." + chatOptions.FileExtension);
@@ -533,16 +535,16 @@ private void EnqueueDataList()
CropBeginning = false,
CropEnding = false
};
- downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, taskData.Title, taskData.Id, taskData.Time, taskData.Streamer,
- downloadOptions.CropBeginning ? TimeSpan.FromSeconds(downloadOptions.CropBeginningTime) : TimeSpan.Zero,
- downloadOptions.CropEnding ? TimeSpan.FromSeconds(downloadOptions.CropEndingTime) : TimeSpan.FromSeconds(taskData.Length),
- taskData.Views.ToString(), taskData.Game) + "." + downloadOptions.FileExtension);
if (radioJson.IsChecked == true)
downloadOptions.DownloadFormat = ChatFormat.Json;
else if (radioHTML.IsChecked == true)
downloadOptions.DownloadFormat = ChatFormat.Html;
else
downloadOptions.DownloadFormat = ChatFormat.Text;
+ downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, taskData.Title, taskData.Id, taskData.Time, taskData.Streamer,
+ downloadOptions.CropBeginning ? TimeSpan.FromSeconds(downloadOptions.CropBeginningTime) : TimeSpan.Zero,
+ downloadOptions.CropEnding ? TimeSpan.FromSeconds(downloadOptions.CropEndingTime) : TimeSpan.FromSeconds(taskData.Length),
+ taskData.Views.ToString(), taskData.Game) + "." + downloadOptions.FileExtension);
ChatDownloadTask downloadTask = new ChatDownloadTask
{
From 193b0ca35411e24f2f9b7200ceab3033ff69600c Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Sun, 15 Oct 2023 01:15:17 -0400
Subject: [PATCH 40/50] I promise this is the last time I touch the dark title
bar code
---
TwitchDownloaderWPF/Services/NativeFunctions.cs | 4 ++--
TwitchDownloaderWPF/Services/ThemeService.cs | 8 +++++++-
TwitchDownloaderWPF/TwitchDownloaderWPF.csproj | 1 +
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/TwitchDownloaderWPF/Services/NativeFunctions.cs b/TwitchDownloaderWPF/Services/NativeFunctions.cs
index 332fe2a5..d4a4e08d 100644
--- a/TwitchDownloaderWPF/Services/NativeFunctions.cs
+++ b/TwitchDownloaderWPF/Services/NativeFunctions.cs
@@ -5,9 +5,9 @@
namespace TwitchDownloaderWPF.Services
{
[SupportedOSPlatform("windows")]
- public static class NativeFunctions
+ public static unsafe class NativeFunctions
{
[DllImport("dwmapi.dll", EntryPoint = "DwmSetWindowAttribute", PreserveSig = true)]
- public static extern int SetWindowAttribute(IntPtr handle, int attribute, [In] ref int attributeValue, int attributeSize);
+ public static extern int SetWindowAttribute(IntPtr handle, int attribute, void* attributeValue, uint attributeSize);
}
}
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Services/ThemeService.cs b/TwitchDownloaderWPF/Services/ThemeService.cs
index a8c80d86..37708653 100644
--- a/TwitchDownloaderWPF/Services/ThemeService.cs
+++ b/TwitchDownloaderWPF/Services/ThemeService.cs
@@ -103,7 +103,13 @@ public void SetTitleBarTheme(WindowCollection windows)
foreach (Window window in windows)
{
var windowHandle = new WindowInteropHelper(window).Handle;
- NativeFunctions.SetWindowAttribute(windowHandle, darkTitleBarAttribute, ref shouldUseDarkTitleBar, sizeof(int));
+ if (windowHandle == IntPtr.Zero)
+ continue;
+
+ unsafe
+ {
+ _ = NativeFunctions.SetWindowAttribute(windowHandle, darkTitleBarAttribute, &shouldUseDarkTitleBar, sizeof(int));
+ }
}
Window wnd = new()
diff --git a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
index c2f05537..00e51d42 100644
--- a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
+++ b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
@@ -21,6 +21,7 @@
true
false
AnyCPU;x64
+ true
From 23f26151d6d7711982ae7676b8741d7d53f1193d Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Sun, 15 Oct 2023 01:16:17 -0400
Subject: [PATCH 41/50] Reuse the theme switcher brush converter
---
TwitchDownloaderWPF/Services/ThemeService.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/TwitchDownloaderWPF/Services/ThemeService.cs b/TwitchDownloaderWPF/Services/ThemeService.cs
index 37708653..015a2cd1 100644
--- a/TwitchDownloaderWPF/Services/ThemeService.cs
+++ b/TwitchDownloaderWPF/Services/ThemeService.cs
@@ -141,12 +141,13 @@ private void ChangeThemePath(string newTheme)
var xmlReader = new XmlSerializer(typeof(ThemeResourceDictionaryModel));
using var streamReader = new StreamReader(themeFile);
var themeValues = (ThemeResourceDictionaryModel)xmlReader.Deserialize(streamReader)!;
+ var brushConverter = new BrushConverter();
foreach (var solidBrush in themeValues.SolidColorBrush)
{
try
{
- _wpfApplication.Resources[solidBrush.Key] = (SolidColorBrush)new BrushConverter().ConvertFrom(solidBrush.Color);
+ _wpfApplication.Resources[solidBrush.Key] = (SolidColorBrush)brushConverter.ConvertFrom(solidBrush.Color);
}
catch (FormatException) { }
}
From 86cd00e92a1e7f523f49dce77114b8122de67877 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Sun, 15 Oct 2023 01:22:04 -0400
Subject: [PATCH 42/50] Fix crash due to negative caret index when computing
text box triple click. Fixes #854
---
.../Behaviors/TextBoxTripleClickBehavior.cs | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/TwitchDownloaderWPF/Behaviors/TextBoxTripleClickBehavior.cs b/TwitchDownloaderWPF/Behaviors/TextBoxTripleClickBehavior.cs
index d4f097be..77c9d880 100644
--- a/TwitchDownloaderWPF/Behaviors/TextBoxTripleClickBehavior.cs
+++ b/TwitchDownloaderWPF/Behaviors/TextBoxTripleClickBehavior.cs
@@ -41,8 +41,15 @@ private static (int start, int length) GetCurrentLine(TextBox textBox)
var caretPos = textBox.CaretIndex;
var text = textBox.Text;
- var start = text.LastIndexOf('\n', caretPos, caretPos);
- var end = text.IndexOf('\n', caretPos);
+ var start = -1;
+ var end = -1;
+
+ // CaretIndex can be negative for some reason.
+ if (caretPos >= 0)
+ {
+ start = text.LastIndexOf('\n', caretPos, caretPos);
+ end = text.IndexOf('\n', caretPos);
+ }
if (start == -1)
{
From 30ed7faca2025c7c6d66022f424cb11f2d5b8455 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mahmut=20S=C3=B6zen?=
<37031361+Teknoist@users.noreply.github.com>
Date: Mon, 16 Oct 2023 01:41:53 +0300
Subject: [PATCH 43/50] minor translation additions (#853)
* Update Strings.tr.resx
* Create README_tr.md
* Update README.md
---
README.md | 3 +-
README_tr.md | 198 ++++++++++++++++++
.../Translations/Strings.tr.resx | 6 +-
3 files changed, 203 insertions(+), 4 deletions(-)
create mode 100644 README_tr.md
diff --git a/README.md b/README.md
index 4f6cf979..6ff1fc4f 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,8 @@
-[**Readme in Spanish**](README_es.md)
+[**Readme in Spanish**](README_es.md)
+[**Readme in Turkish**](README_tr.md)
## Chat Render Example
diff --git a/README_tr.md b/README_tr.md
new file mode 100644
index 00000000..0b2cb7cf
--- /dev/null
+++ b/README_tr.md
@@ -0,0 +1,198 @@
+
+
+
+
+
+
Twitch İndirici
+
+
+ Twitch VOD/Clip/Chat İndirici ve Chat Oynatıcı
+
+
+ Hata Bildir
+
+
+
+[**İspanyolca'da Oku**](README_es.md)
+[**İngilizce'de Oku**](README.md)
+
+## Chat Oynatma Örneği
+
+https://user-images.githubusercontent.com/1060681/197653099-c3fd12c2-f03a-4580-84e4-63ce3f36be8d.mp4
+
+## Neler Yapabilir?
+
+- Twitch VOD'larını İndir
+- Twitch Kliplerini İndir
+- VOD'lar ve Klipler için sohbeti, ya [tüm orijinal bilgileri içeren bir JSON olarak](https://pastebin.com/raw/YDgRe6X4), bir tarayıcı HTML dosyası olarak ya da [düz metin dosyası olarak](https://pastebin.com/raw/016azeQX) indirin.
+- Daha önce oluşturulmuş bir JSON sohbet dosyasının içeriğini güncelleyin ve başka bir biçimde kaydetme seçeneğiyle kaydedin.
+- Daha önce oluşturulmuş bir JSON sohbet dosyasını kullanarak sohbeti Twitter Twemoji veya Google Noto Color emojileri ve BTTV, FFZ, 7TV statik ve animasyonlu emojilerle oynatmak için kullanın.
+
+# GUI
+
+## Windows WPF
+
+![](https://i.imgur.com/bLegxGX.gif)
+
+### [Full WPF belgelerini buradan görüntüleyin](TwitchDownloaderWPF/README.md).
+
+### İşlevsellik
+
+Windows WPF GUI, programın tüm ana işlevlerini ve bazı ek yaşam kalitesi işlevlerini uygular:
+- Aynı anda çalıştırılacak birden fazla indirme/oynatma işini sıraya alın.
+- VOD/Klip bağlantıları listesinden indirme işlerinin bir listesini oluşturun.
+- Uygulamayı terk etmeden herhangi bir yayıcıdan birden fazla VOD/klip arayın ve indirin.
+
+### Çoklu Dil Desteği
+
+Windows WPF GUI, topluluk çevirileri sayesinde birçok dilde kullanılabilir. Daha fazla ayrıntı için [WPF README](TwitchDownloaderWPF/README.md)'nin [Yerelleştirme bölümüne](TwitchDownloaderWPF/README.md#localization) bakın.
+
+### Temalar
+
+Windows WPF GUI, hem açık hem de karanlık temalar ile gelir ve mevcut Windows temasına göre canlı olarak güncelleme seçeneği sunar. Ayrıca kullanıcı tarafından oluşturulan temaları destekler! Daha fazla ayrıntı için [WPF README](TwitchDownloaderWPF/README.md)'nin [Tema bölümüne](TwitchDownloaderWPF/README.md#theming) bakın.
+
+### Video Gösterimi
+
+https://www.youtube.com/watch?v=0W3MhfhnYjk
+(eski sürüm, aynı konsept)
+
+## Linux?
+
+***Nasıl cevireceğimi bilemedim terminal versionu var [githubda](https://github.com/mohad12211/twitch-downloader-gui) gidin ona bakın diyor kısaca birde [AUR'da](https://aur.archlinux.org/packages/twitch-downloader-gui) terminalin biraz süslü gui hali var ona bakabilirsniiz diyor.
+
+## MacOS?
+
+Malesef MacOS için henüz bir GUI mevcut değil :(
+
+# CLI
+
+### [Tüm CLI belgelerini buradan inceleyin](TwitchDownloaderCLI/README.md).
+
+CLI, ana program işlevlerini uygulayan ve Windows, Linux ve MacOS* üzerinde çalışan çapraz platformlu bir araçtır.
+
+*Sadece Intel Mac'ler test edilmiştir
+
+CLI ile, harici komut dosyalarını kullanarak video işleme işlemini otomatikleştirmek mümkündür. Örneğin, Windows'ta bir `.bat` dosyasına aşağıdaki kodu kopyalayarak bir VOD'u ve onun sohbetini indirebilir ve ardından sohbeti renderlayabilirsiniz.
+```bat
+@echo off
+set /p vodid="VOD Kimliğini Girin: "
+TwitchDownloaderCLI.exe videodownload --id %vodid% --ffmpeg-path "ffmpeg.exe" -o %vodid%.mp4
+TwitchDownloaderCLI.exe chatdownload --id %vodid% -o %vodid%_chat.json -E
+TwitchDownloaderCLI.exe chatrender -i %vodid%_chat.json -h 1080 -w 422 --framerate 30 --update-rate 0 --font-size 18 -o %vodid%_chat.mp4
+```
+
+## Windows - Başlangıç
+
+1. [Releases-Sürümler](https://github.com/lay295/TwitchDownloader/releases/) sayfasına gidin ve en son Windows sürümünü indirin veya [kaynaktan derleyin.](#building-from-source).
+2. `TwitchDownloaderCLI.exe`'yi çıkartın.
+3. Dosyayı çıkardığınız yerde terminal açın.
+4. FFmpeg'e sahip değilseniz,[Chocolatey package manager](https://community.chocolatey.org/) aracılığı ile indirebilir veya bağımsız bir dosya olarak [ffmpeg.org](https://ffmpeg.org/download.html) adresinden alabilir veya TwitchDownloaderCLI kullanarak alabilirsiniz. Şu komutu kullanarak indirebilirsiniz:
+```
+TwitchDownloaderCLI.exe ffmpeg --download
+```
+5. Artık indirme işlemine başlayabilirsiniz, örneğin:
+```
+TwitchDownloaderCLI.exe videodownload --id -o out.mp4
+```
+
+## Linux – Başlangıç
+
+1. Bazı dağıtımlar, Linux Alpine gibi, bazı diller için (Arapça, Farsça, Tayca vb.) yazı tiplerini eksik bulabilir. Bu durum sizin için geçerliyse, [Noto](https://fonts.google.com/noto/specimen/Noto+Sans) gibi ek yazı tipleri ailesi yükleyin veya dağıtımınızın yazı tipleri hakkındaki wiki sayfasını kontrol edin, çünkü bu özel senaryo için bir kurulum komutuna sahip olabilir, örneğin [Linux Alpine](https://wiki.alpinelinux.org/wiki/Fonts) yazı tipi sayfası gibi.
+2. `fontconfig` ve `libfontconfig1`'in yüklü olduğundan emin olun. Ubuntu'da `apt-get install fontconfig libfontconfig1` kullanabilirsiniz.
+3. [Sürümler](https://github.com/lay295/TwitchDownloader/releases/) sayfasına gidin ve Linux için en son ikili sürümü indirin, Arch Linux için [AUR Paketi](https://aur.archlinux.org/packages/twitch-downloader-bin/)ni alın veya [kaynaktan derleyin](#building-from-source).
+4. `TwitchDownloaderCLI`'yi çıkarın.
+5. Dosyayı çıkardığınız yere gidin ve terminalde çalıştırılabilir izinleri verin:
+```
+sudo chmod +x TwitchDownloaderCLI
+```
+6. a) Eğer FFmpeg'e sahip değilseniz, bunu dağıtım paket yöneticiniz aracılığıyla kurmalısınız. Ayrıca, [ffmpeg.org](https://ffmpeg.org/download.html) adresinden bağımsız bir dosya olarak veya TwitchDownloaderCLI kullanarak da edinebilirsiniz.
+```
+./TwitchDownloaderCLI ffmpeg --download
+```
+6. b) Bağımsız bir dosya olarak indirildiyse, ona çalıştırılabilir izinler vermelisiniz:
+```
+sudo chmod +x ffmpeg
+```
+7. Şimdi indiriciyi kullanmaya başlayabilirsiniz, örneğin:
+```
+./TwitchDownloaderCLI videodownload --id -o out.mp4
+```
+## MacOS – Başlangıç
+1. [Releases](https://github.com/lay295/TwitchDownloader/releases/) sayfasına gidin ve MacOS için en son sürümü indirin veya kaynaktan derleyin.
+2. `TwitchDownloaderCLI` dosyasını çıkarın.
+3. Dosyayı çıkardığınız yere terminalde çalıştırılabilir izinler verin.
+```
+chmod +x TwitchDownloaderCLI
+```
+4. a) Eğer FFmpeg'e sahip değilseniz, [Homebrew paket yöneticisi](https://brew.sh/) aracılığıyla kurabilirsiniz veya bağımsız bir dosya olarak [ffmpeg.org](https://ffmpeg.org/download.html) adresinden veya TwitchDownloaderCLI kullanarak edinebilirsiniz.
+```
+./TwitchDownloaderCLI ffmpeg --download
+```
+4. b) Bağımsız bir dosya olarak indirildiyse, ona çalıştırılabilir izinler vermelisiniz.
+```
+chmod +x ffmpeg
+```
+5. Şimdi indiriciyi kullanmaya başlayabilirsiniz, örneğin:
+```
+./TwitchDownloaderCLI videodownload --id -o out.mp4
+```
+
+# Kaynaktan derleme
+
+## Gereksinimler
+
+- [.NET 6.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
+
+## Derleme Talimatları
+
+1. Depoyu klonlayın:
+```
+git clone https://github.com/lay295/TwitchDownloader.git
+```
+2. Çözüm klasörüne gidin:
+```
+cd TwitchDownloader
+```
+3. Çözümü geri yükleyin:
+```
+dotnet restore
+```
+4. a) GUI'yi oluşturun:
+```
+dotnet publish TwitchDownloaderWPF -p:PublishProfile=Windows -p:DebugType=None -p:DebugSymbols=false
+```
+4. b) CLI'yi oluşturun:
+```
+dotnet publish TwitchDownloaderCLI -p:PublishProfile= -p:DebugType=None -p:DebugSymbols=false
+```
+- Uygulanabilir Profiller: `Windows`, `Linux`, `LinuxAlpine`, `LinuxArm`, `LinuxArm64`, `MacOS`
+5. a) GUI derleme klasörüne gidin:
+```
+cd TwitchDownloaderWPF/bin/Release/net6.0-windows/publish/win-x64
+```
+5. b) CLI derleme klasörüne gidin:
+```
+cd TwitchDownloaderCLI/bin/Release/net6.0/publish
+```
+
+# Lisans
+
+[MIT](./LICENSE.txt)
+
+# Üçüncü Taraf Kredileri
+
+Sohbet Görüntülemeleri, [SkiaSharp ve HarfBuzzSharp](https://github.com/mono/SkiaSharp) tarafından oluşturulmuştur © Microsoft Corporation.
+
+Sohbet Görüntülemeleri işlenmesi ve Video İndirmeleri [FFmpeg](https://ffmpeg.org/) ile sonlandırılır © FFmpeg geliştiricileri.
+
+Sohbet Görüntülemeleri, [Noto Renkli Emoji](https://github.com/googlefonts/noto-emoji) tarafından kullanılabilir © Google ve katkıda bulunanlar.
+
+Sohbet Görüntülemeleri, [Twemoji](https://github.com/twitter/twemoji) tarafından kullanılabilir © Twitter ve katkıda bulunanlar.
+
+Paketlenmiş FFmpeg ikili dosyaları [gyan.dev](https://www.gyan.dev/ffmpeg/) adresinden alınmıştır © Gyan Doshi.
+
+Alınan FFmpeg ikili dosyaları çalışma zamanında [Xabe.FFmpeg.Downloader](https://github.com/tomaszzmuda/Xabe.FFmpeg) kullanılarak indirilir © Xabe.
+
+Sohbet HTML dışa aktarmaları, [Google Fonts API](https://fonts.google.com/) tarafından barındırılan _Inter_ yazı tipini kullanır © Google.
+
+Kullanılan tüm harici kütüphanelerin tam listesi için [THIRD-PARTY-LICENSES.txt](./TwitchDownloaderCore/Resources/THIRD-PARTY-LICENSES.txt) dosyasına bakınız.
diff --git a/TwitchDownloaderWPF/Translations/Strings.tr.resx b/TwitchDownloaderWPF/Translations/Strings.tr.resx
index c830884c..e4acd7b5 100644
--- a/TwitchDownloaderWPF/Translations/Strings.tr.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.tr.resx
@@ -773,9 +773,9 @@
Windows uygulama teması izleyicisi başlatılamıyor. Hata kodu: {0}
- Videos per page:
+ Sayfa başına video:
- Downloading FFmpeg {0}%
+ FFmpeg İndiriliyor {0}%
-
\ No newline at end of file
+
From f4fcaf27ca6fba3d09d2dc8a357b085f0c6d95f6 Mon Sep 17 00:00:00 2001
From: ScrubN <72096833+ScrubN@users.noreply.github.com>
Date: Wed, 18 Oct 2023 19:26:02 -0400
Subject: [PATCH 44/50] Update build instructions
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 6ff1fc4f..adfde98d 100644
--- a/README.md
+++ b/README.md
@@ -160,6 +160,7 @@ cd TwitchDownloader
```
dotnet restore
```
+- Non-Windows devices may need to explicitly specify a project to restore, i.e. `dotnet restore TwitchDownloaderCLI`
4. a) Build the GUI:
```
dotnet publish TwitchDownloaderWPF -p:PublishProfile=Windows -p:DebugType=None -p:DebugSymbols=false
From d6dbe78febc28342c35f21417ac3cf0dbf3ef92f Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Sat, 21 Oct 2023 13:10:48 -0400
Subject: [PATCH 45/50] Support highlighting new bit badge notifications (#869)
* Simplify icon generation
* Cleanup
* Add support for bit badge tier notifications
* More simplification
* Even more simplification
---
TwitchDownloaderCore/ChatRenderer.cs | 56 ++++++++++--
TwitchDownloaderCore/Tools/HighlightIcons.cs | 90 ++++++--------------
2 files changed, 73 insertions(+), 73 deletions(-)
diff --git a/TwitchDownloaderCore/ChatRenderer.cs b/TwitchDownloaderCore/ChatRenderer.cs
index 4059f8b4..1516082e 100644
--- a/TwitchDownloaderCore/ChatRenderer.cs
+++ b/TwitchDownloaderCore/ChatRenderer.cs
@@ -27,8 +27,7 @@ public sealed class ChatRenderer : IDisposable
public bool Disposed { get; private set; } = false;
public ChatRoot chatRoot { get; private set; } = new ChatRoot();
- private const string PURPLE = "#7B2CF2";
- private static readonly SKColor Purple = SKColor.Parse(PURPLE);
+ private static readonly SKColor Purple = SKColor.Parse("#7B2CF2");
private static readonly string[] DefaultUsernameColors = { "#FF0000", "#0000FF", "#00FF00", "#B22222", "#FF7F50", "#9ACD32", "#FF4500", "#2E8B57", "#DAA520", "#D2691E", "#5F9EA0", "#1E90FF", "#FF69B4", "#8A2BE2", "#00FF7F" };
private static readonly Regex RtlRegex = new("[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]", RegexOptions.Compiled);
@@ -704,6 +703,9 @@ private void DrawAccentedMessage(Comment comment, List<(SKImageInfo info, SKBitm
case HighlightType.SubscribedPrime:
DrawSubscribeMessage(comment, sectionImages, emotePositionList, ref drawPos, defaultPos, highlightIcon, iconPoint);
break;
+ case HighlightType.BitBadgeTierNotification:
+ DrawBitsBadgeTierMessage(comment, sectionImages, emotePositionList, ref drawPos, defaultPos, highlightIcon, iconPoint);
+ break;
case HighlightType.GiftedMany:
case HighlightType.GiftedSingle:
case HighlightType.GiftedAnonymous:
@@ -735,7 +737,7 @@ private void DrawSubscribeMessage(Comment comment, List<(SKImageInfo info, SKBit
drawPos.X += highlightIcon.Width + renderOptions.WordSpacing;
defaultPos.X = drawPos.X;
- DrawUsername(comment, sectionImages, ref drawPos, defaultPos, false, PURPLE);
+ DrawUsername(comment, sectionImages, ref drawPos, defaultPos, false, Purple);
AddImageSection(sectionImages, ref drawPos, defaultPos);
// Remove the commenter's name from the resub message
@@ -765,6 +767,41 @@ private void DrawSubscribeMessage(Comment comment, List<(SKImageInfo info, SKBit
DrawNonAccentedMessage(customResubMessage, sectionImages, emotePositionList, false, ref drawPos, ref defaultPos);
}
+ private void DrawBitsBadgeTierMessage(Comment comment, List<(SKImageInfo info, SKBitmap bitmap)> sectionImages, List<(Point, TwitchEmote)> emotePositionList, ref Point drawPos, Point defaultPos, SKImage highlightIcon, Point iconPoint)
+ {
+ using SKCanvas canvas = new(sectionImages.Last().bitmap);
+
+ canvas.DrawImage(highlightIcon, iconPoint.X, iconPoint.Y);
+ drawPos.X += highlightIcon.Width + renderOptions.WordSpacing;
+ defaultPos.X = drawPos.X;
+
+ if (comment.message.fragments.Count == 1)
+ {
+ DrawUsername(comment, sectionImages, ref drawPos, defaultPos, false, messageFont.Color);
+
+ var bitsBadgeVersion = comment.message.user_badges.FirstOrDefault(x => x._id == "bits")?.version;
+ if (bitsBadgeVersion is not null)
+ {
+ comment.message.body = bitsBadgeVersion.Length > 3
+ ? $"just earned a new {bitsBadgeVersion.AsSpan(0, bitsBadgeVersion.Length - 3)}K Bits badge!"
+ : $"just earned a new {bitsBadgeVersion} Bits badge!";
+ }
+ else
+ {
+ comment.message.body = "just earned a new Bits badge!";
+ }
+
+ comment.message.fragments[0].text = comment.message.body;
+ }
+ else
+ {
+ // This should never be possible, but just in case.
+ DrawUsername(comment, sectionImages, ref drawPos, defaultPos, true, messageFont.Color);
+ }
+
+ DrawMessage(comment, sectionImages, emotePositionList, false, ref drawPos, defaultPos);
+ }
+
private void DrawGiftMessage(Comment comment, List<(SKImageInfo info, SKBitmap bitmap)> sectionImages, List<(Point, TwitchEmote)> emotePositionList, ref Point drawPos, Point defaultPos, SKImage highlightIcon, Point iconPoint)
{
using SKCanvas canvas = new(sectionImages.Last().bitmap);
@@ -1291,22 +1328,25 @@ private static float MeasureRtlText(ReadOnlySpan rtlText, SKPaint textFont
return measure.Width;
}
- private void DrawUsername(Comment comment, List<(SKImageInfo info, SKBitmap bitmap)> sectionImages, ref Point drawPos, Point defaultPos, bool appendColon = true, string colorOverride = null)
+ private void DrawUsername(Comment comment, List<(SKImageInfo info, SKBitmap bitmap)> sectionImages, ref Point drawPos, Point defaultPos, bool appendColon = true, SKColor? colorOverride = null)
{
- SKColor userColor = SKColor.Parse(colorOverride ?? comment.message.user_color ?? DefaultUsernameColors[Math.Abs(comment.commenter.display_name.GetHashCode()) % DefaultUsernameColors.Length]);
+ var userColor = colorOverride ?? SKColor.Parse(comment.message.user_color ?? DefaultUsernameColors[Math.Abs(comment.commenter.display_name.GetHashCode()) % DefaultUsernameColors.Length]);
if (colorOverride is null)
- userColor = GenerateUserColor(userColor, renderOptions.BackgroundColor, renderOptions);
+ userColor = AdjustColorVisibility(userColor, renderOptions.BackgroundColor, renderOptions);
using SKPaint userPaint = comment.commenter.display_name.Any(IsNotAscii)
? GetFallbackFont(comment.commenter.display_name.First(IsNotAscii)).Clone()
: nameFont.Clone();
userPaint.Color = userColor;
- string userName = comment.commenter.display_name + (appendColon ? ":" : "");
+ var userName = appendColon
+ ? comment.commenter.display_name + ":"
+ : comment.commenter.display_name;
+
DrawText(userName, userPaint, true, sectionImages, ref drawPos, defaultPos, false);
}
- private static SKColor GenerateUserColor(SKColor userColor, SKColor backgroundColor, ChatRenderOptions renderOptions)
+ private static SKColor AdjustColorVisibility(SKColor userColor, SKColor backgroundColor, ChatRenderOptions renderOptions)
{
backgroundColor.ToHsl(out _, out _, out float backgroundBrightness);
userColor.ToHsl(out float userHue, out float userSaturation, out float userBrightness);
diff --git a/TwitchDownloaderCore/Tools/HighlightIcons.cs b/TwitchDownloaderCore/Tools/HighlightIcons.cs
index ee1bc761..71100dfe 100644
--- a/TwitchDownloaderCore/Tools/HighlightIcons.cs
+++ b/TwitchDownloaderCore/Tools/HighlightIcons.cs
@@ -18,27 +18,30 @@ public enum HighlightType
PayingForward,
ChannelPointHighlight,
Raid,
+ BitBadgeTierNotification,
Unknown
}
public sealed class HighlightIcons : IDisposable
{
- public bool Disposed { get; private set; } = false;
+ public bool Disposed { get; private set; }
private const string SUBSCRIBED_TIER_ICON_SVG = "m 32.599229,13.144498 c 1.307494,-2.80819 5.494049,-2.80819 6.80154,0 l 5.648628,12.140919 13.52579,1.877494 c 3.00144,0.418654 4.244522,3.893468 2.138363,5.967405 -3.357829,3.309501 -6.715662,6.618992 -10.073491,9.928491 L 53.07148,56.81637 c 0.524928,2.962772 -2.821092,5.162303 -5.545572,3.645496 L 36,54.043603 24.474093,60.461866 C 21.749613,61.975455 18.403591,59.779142 18.92852,56.81637 L 21.359942,43.058807 11.286449,33.130316 c -2.1061588,-2.073937 -0.863074,-5.548751 2.138363,-5.967405 l 13.52579,-1.877494 z";
private const string SUBSCRIBED_PRIME_ICON_SVG = "m 61.894653,21.663055 v 25.89488 c 0,3.575336 -2.898361,6.47372 -6.473664,6.47372 H 16.57901 c -3.573827,-0.0036 -6.470094,-2.89986 -6.473663,-6.47372 V 21.663055 L 23.052674,31.373635 36,18.426194 c 4.315772,4.315816 8.631553,8.631629 12.947323,12.947441 z";
private const string GIFTED_SINGLE_ICON_SVG = "m 55.187956,23.24523 h 6.395987 V 42.433089 H 58.38595 V 61.620947 H 13.614042 V 42.433089 H 10.416049 V 23.24523 h 6.395987 v -3.859957 c 0,-8.017328 9.689919,-12.0307888 15.359963,-6.363975 0.418936,0.418935 0.796298,0.879444 1.125692,1.371934 l 2.702305,4.055034 2.702305,-4.055034 a 8.9863623,8.9863139 0 0 1 1.125692,-1.371934 c 5.666845,-5.6668138 15.359963,-1.653353 15.359963,6.363975 z M 23.208023,19.385273 v 3.859957 h 8.301992 l -3.536982,-5.305444 a 2.6031666,2.6031528 0 0 0 -4.76501,1.445487 z m 25.583946,0 v 3.859957 h -8.301991 l 3.536983,-5.305444 a 2.6031666,2.6031528 0 0 1 4.765008,1.442286 z m 6.395987,10.255909 v 6.395951 H 39.19799 v -6.395951 z m -3.197992,25.58381 V 42.433089 H 39.19799 V 55.224992 Z M 32.802003,29.641182 v 6.395951 H 16.812036 v -6.395951 z m 0,12.791907 H 20.010028 v 12.791903 h 12.791975 z";
private const string GIFTED_MANY_ICON_URL = "https://static-cdn.jtvnw.net/subs-image-assets/gift-illus.png";
private const string GIFTED_ANONYMOUS_ICON_SVG = "m 54.571425,64.514958 a 4.3531428,4.2396967 0 0 1 -1.273998,-0.86096 l -1.203426,-1.172067 a 7.0051428,6.822584 0 0 0 -9.90229,0 c -3.417139,3.328092 -8.962569,3.328092 -12.383427,0 l -0.159707,-0.155553 a 7.1871427,6.9998405 0 0 0 -9.854005,-0.28216 l -1.894286,1.635103 a 4.9362858,4.8076423 0 0 1 -3.276,1.215474 H 10 V 32.337399 a 26.000001,25.322423 0 0 1 52,0 v 32.557396 h -5.627146 c -0.627714,0 -1.240569,-0.133847 -1.801429,-0.379837 z M 35.999996,14.249955 A 18.571428,18.087444 0 0 0 17.428572,32.337399 v 22.515245 a 14.619428,14.238435 0 0 1 17.471998,2.358609 l 0.163448,0.155554 c 0.516285,0.50645 1.355715,0.50645 1.875712,0 a 14.437428,14.061179 0 0 1 17.631712,-2.11623 V 32.337399 A 18.571428,18.087444 0 0 0 35.999996,14.249955 Z M 24.857142,35.954887 a 3.7142855,3.6174889 0 1 1 7.42857,0 3.7142855,3.6174889 0 0 1 -7.42857,0 z m 18.571432,-3.617488 a 3.7142859,3.6174892 0 1 0 0,7.234978 3.7142859,3.6174892 0 0 0 0,-7.234978 z";
+ private const string BIT_BADGE_TIER_NOTIFICATION_ICON_SVG = "M 14.242705,42.37453 36,11.292679 57.757295,42.37453 36,61.023641 Z M 22.566425,41.323963 36,22.13092 49.433577,41.317747 46.79162,43.580506 36,39.266345 25.205273,43.586723 22.566425,41.320854 Z";
- private static readonly Regex SubMessageRegex = new(@"^(subscribed (?:with Prime|at Tier \d)\. They've subscribed for \d?\d?\d months(?:, currently on a \d?\d?\d month streak)?! )(.+)$", RegexOptions.Compiled);
- private static readonly Regex GiftAnonymousRegex = new(@"^An anonymous user (?:gifted a|is gifting \d\d?\d?) Tier \d", RegexOptions.Compiled);
+ private static readonly Regex SubMessageRegex = new(@"^(subscribed (?:with Prime|at Tier \d)\. They've subscribed for \d{1,3} months(?:, currently on a \d{1,3} month streak)?! )(.+)$", RegexOptions.Compiled);
+ private static readonly Regex GiftAnonymousRegex = new(@"^An anonymous user (?:gifted a|is gifting \d{1,4}) Tier \d", RegexOptions.Compiled);
private SKImage _subscribedTierIcon;
private SKImage _subscribedPrimeIcon;
private SKImage _giftSingleIcon;
private SKImage _giftManyIcon;
private SKImage _giftAnonymousIcon;
+ private SKImage _bitBadgeTierNotificationIcon;
private readonly string _cachePath;
private readonly SKColor _purple;
@@ -54,8 +57,6 @@ public HighlightIcons(string cachePath, SKColor iconPurple, bool offline)
// If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck
public static HighlightType GetHighlightType(Comment comment)
{
- const string ANONYMOUS_GIFT_ACCOUNT_ID = "274598607"; // '274598607' is the id of the anonymous gift message account, display name: 'AnAnonymousGifter'
-
if (comment.message.body.Length == 0)
{
// This likely happens due to the 7TV extension letting users bypass the IRC message trimmer
@@ -104,6 +105,9 @@ public static HighlightType GetHighlightType(Comment comment)
}
}
+ if (bodySpan.Equals("bits badge tier notification ", StringComparison.Ordinal))
+ return HighlightType.BitBadgeTierNotification;
+
if (char.IsDigit(bodySpan[0]) && bodySpan.Contains("have joined!", StringComparison.Ordinal))
{
// TODO: use bodySpan when .NET 7
@@ -111,61 +115,29 @@ public static HighlightType GetHighlightType(Comment comment)
return HighlightType.Raid;
}
+ const string ANONYMOUS_GIFT_ACCOUNT_ID = "274598607"; // Display name is 'AnAnonymousGifter'
if (comment.commenter._id is ANONYMOUS_GIFT_ACCOUNT_ID && GiftAnonymousRegex.IsMatch(comment.message.body))
return HighlightType.GiftedAnonymous;
return HighlightType.None;
}
- /// A the requested icon or null if no icon exists for the highlight type
- /// The icon returned is NOT a copy and should not be manually disposed.
+ /// The requested icon or if no icon exists for the highlight type
+ /// The returned is NOT a copy and should not be manually disposed.
public SKImage GetHighlightIcon(HighlightType highlightType, SKColor textColor, double fontSize)
{
- // Return the needed icon from cache or generate if null
return highlightType switch
{
- HighlightType.SubscribedTier => _subscribedTierIcon ?? GenerateHighlightIcon(highlightType, textColor, fontSize),
- HighlightType.SubscribedPrime => _subscribedPrimeIcon ?? GenerateHighlightIcon(highlightType, textColor, fontSize),
- HighlightType.GiftedSingle => _giftSingleIcon ?? GenerateHighlightIcon(highlightType, textColor, fontSize),
- HighlightType.GiftedMany => _giftManyIcon ?? GenerateHighlightIcon(highlightType, textColor, fontSize),
- HighlightType.GiftedAnonymous => _giftAnonymousIcon ?? GenerateHighlightIcon(highlightType, textColor, fontSize),
+ HighlightType.SubscribedTier => _subscribedTierIcon ??= GenerateSvgIcon(SUBSCRIBED_TIER_ICON_SVG, textColor, fontSize),
+ HighlightType.SubscribedPrime => _subscribedPrimeIcon ??= GenerateSvgIcon(SUBSCRIBED_PRIME_ICON_SVG, _purple, fontSize),
+ HighlightType.GiftedSingle => _giftSingleIcon ??= GenerateSvgIcon(GIFTED_SINGLE_ICON_SVG, textColor, fontSize),
+ HighlightType.GiftedMany => _giftManyIcon ??= GenerateGiftedManyIcon(fontSize, _cachePath, _offline),
+ HighlightType.GiftedAnonymous => _giftAnonymousIcon ??= GenerateSvgIcon(GIFTED_ANONYMOUS_ICON_SVG, textColor, fontSize),
+ HighlightType.BitBadgeTierNotification => _bitBadgeTierNotificationIcon ??= GenerateSvgIcon(BIT_BADGE_TIER_NOTIFICATION_ICON_SVG, textColor, fontSize),
_ => null
};
}
- private SKImage GenerateHighlightIcon(HighlightType highlightType, SKColor textColor, double fontSize)
- {
- // Generate the needed icon
- var returnIcon = highlightType is HighlightType.GiftedMany
- ? GenerateGiftedManyIcon(fontSize, _cachePath, _offline)
- : GenerateSvgIcon(highlightType, _purple, textColor, fontSize);
-
- // Cache the icon
- switch (highlightType)
- {
- case HighlightType.SubscribedTier:
- _subscribedTierIcon = returnIcon;
- break;
- case HighlightType.SubscribedPrime:
- _subscribedPrimeIcon = returnIcon;
- break;
- case HighlightType.GiftedSingle:
- _giftSingleIcon = returnIcon;
- break;
- case HighlightType.GiftedMany:
- _giftManyIcon = returnIcon;
- break;
- case HighlightType.GiftedAnonymous:
- _giftAnonymousIcon = returnIcon;
- break;
- default:
- throw new NotSupportedException("The requested highlight icon does not exist.");
- }
-
- // Return the generated icon
- return returnIcon;
- }
-
private static SKImage GenerateGiftedManyIcon(double fontSize, string cachePath, bool offline)
{
//int newSize = (int)(fontSize / 0.2727); // 44*44px @ 12pt font // Doesn't work because our image sections aren't tall enough and I'm not rewriting that right now
@@ -192,36 +164,22 @@ private static SKImage GenerateGiftedManyIcon(double fontSize, string cachePath,
return SKImage.FromBitmap(resizedBitmap);
}
- private static SKImage GenerateSvgIcon(HighlightType highlightType, SKColor purple, SKColor textColor, double fontSize)
+ private static SKImage GenerateSvgIcon(string iconSvgString, SKColor iconColor, double fontSize)
{
using var tempBitmap = new SKBitmap(72, 72); // Icon SVG strings are scaled for 72x72
using var tempCanvas = new SKCanvas(tempBitmap);
- using var iconPath = SKPath.ParseSvgPathData(highlightType switch
- {
- HighlightType.SubscribedTier => SUBSCRIBED_TIER_ICON_SVG,
- HighlightType.SubscribedPrime => SUBSCRIBED_PRIME_ICON_SVG,
- HighlightType.GiftedSingle => GIFTED_SINGLE_ICON_SVG,
- HighlightType.GiftedAnonymous => GIFTED_ANONYMOUS_ICON_SVG,
- _ => throw new NotSupportedException("The requested icon svg path does not exist.")
- });
+ using var iconPath = SKPath.ParseSvgPathData(iconSvgString);
iconPath.FillType = SKPathFillType.EvenOdd;
- var iconColor = new SKPaint
+ var iconPaint = new SKPaint
{
- Color = highlightType switch
- {
- HighlightType.SubscribedTier => textColor,
- HighlightType.SubscribedPrime => purple,
- HighlightType.GiftedSingle => textColor,
- HighlightType.GiftedAnonymous => textColor,
- _ => throw new NotSupportedException("The requested icon color does not exist.")
- },
+ Color = iconColor,
IsAntialias = true,
LcdRenderText = true
};
- tempCanvas.DrawPath(iconPath, iconColor);
+ tempCanvas.DrawPath(iconPath, iconPaint);
var newSize = (int)(fontSize / 0.6); // 20*20px @ 12pt font
var imageInfo = new SKImageInfo(newSize, newSize);
var resizedBitmap = tempBitmap.Resize(imageInfo, SKFilterQuality.High);
@@ -309,6 +267,7 @@ private void Dispose(bool isDisposing)
_giftSingleIcon?.Dispose();
_giftManyIcon?.Dispose();
_giftAnonymousIcon?.Dispose();
+ _bitBadgeTierNotificationIcon?.Dispose();
// Set the root references to null to explicitly tell the garbage collector that the resources have been disposed
_subscribedTierIcon = null;
@@ -316,6 +275,7 @@ private void Dispose(bool isDisposing)
_giftSingleIcon = null;
_giftManyIcon = null;
_giftAnonymousIcon = null;
+ _bitBadgeTierNotificationIcon = null;
}
}
finally
From 8db2d26554c17b8f37cfc26c668315843fb5d43d Mon Sep 17 00:00:00 2001
From: Eugene <59817721+SKZGx@users.noreply.github.com>
Date: Thu, 26 Oct 2023 01:20:32 +0300
Subject: [PATCH 46/50] Add Ukrainian Translation (#870)
* Ukrainian Translation
* fix comma
---
.../Services/AvailableCultures.cs | 2 +
.../Translations/Strings.uk.resx | 780 ++++++++++++++++++
.../TwitchDownloaderWPF.csproj | 3 +
3 files changed, 785 insertions(+)
create mode 100644 TwitchDownloaderWPF/Translations/Strings.uk.resx
diff --git a/TwitchDownloaderWPF/Services/AvailableCultures.cs b/TwitchDownloaderWPF/Services/AvailableCultures.cs
index 76a25c13..ffdc5cb9 100644
--- a/TwitchDownloaderWPF/Services/AvailableCultures.cs
+++ b/TwitchDownloaderWPF/Services/AvailableCultures.cs
@@ -24,6 +24,7 @@ public static class AvailableCultures
public static readonly Culture Polish;
public static readonly Culture Russian;
public static readonly Culture Turkish;
+ public static readonly Culture Ukrainian;
public static readonly Culture SimplifiedChinese;
public static readonly Culture[] All;
@@ -38,6 +39,7 @@ static AvailableCultures()
Polish = new Culture("pl-PL", "Polski"),
Russian = new Culture("ru-RU", "Русский"),
Turkish = new Culture("tr-TR", "Türkçe"),
+ Ukrainian = new Culture("uk-ua", "Українська"),
SimplifiedChinese = new Culture("zh-CN", "简体中文")
};
}
diff --git a/TwitchDownloaderWPF/Translations/Strings.uk.resx b/TwitchDownloaderWPF/Translations/Strings.uk.resx
new file mode 100644
index 00000000..98839027
--- /dev/null
+++ b/TwitchDownloaderWPF/Translations/Strings.uk.resx
@@ -0,0 +1,780 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Прийняти
+
+
+ Додати в чергу
+
+
+ Ви вибрали альфа-канал (прозорість) для контейнера/кодека, який його не підтримує. Видаліть прозорість або закодуйте за допомогою MOV + RLE/PRORES або WEBM + VP8/VP9
+
+
+ Тема
+ Leave a trailing space
+
+
+ Натисніть, щоб дізнатися, як створити власну тему!
+
+
+ Доступні параметри:
+
+
+ Колір фону:
+
+
+ Заборонені слова
+ Leaving a trailing space
+
+
+ Список заборонених слів або фраз - через кому, пробіли навколо ком ігноруються, НЕ враховує регістр.
+
+
+ Огляд
+
+
+ BTTV смайлики:
+
+
+ Тека кешу:
+
+
+ Чати:
+
+
+ Фільтр значків у чаті:
+
+
+ значки у чаті:
+
+
+ Обрізати:
+
+
+ Завантаження чату
+
+
+ Завантаження чатів
+
+
+ Шрифт:
+
+
+ Розмір шрифту:
+
+
+ Висота:
+
+
+ Промальовування чату
+
+
+ Промальовування чатів
+
+
+ Оновлення чату
+
+
+ Оновлення чату
+
+
+ Ширина:
+
+
+ Очистити
+
+
+ Ви впевнені, що хочете очистити кеш?\nВам слід робити це лише у випадку, якщо програма працює некоректно
+
+
+ Завантаження кліпів
+
+
+ Завантаження кліпів
+
+
+ Посилання/ІН(ID) кліпу:
+
+
+ Кліпи:
+
+
+ Кінець
+
+
+ Початок
+
+
+ Обрізати відео:
+
+
+ форматування date_custom базується на
+
+
+ Стандартні рядки формату дати та часу як у C#
+
+
+ Підтвердити видалення
+
+
+ Часто користуєтеся програмою і хочете мене підтримати? Пригостіть мене кавою :)
+
+
+ Завантажити
+
+
+ З'єднання:
+
+
+ Завантажити шаблони назв файлів:
+
+
+ Формат завантаження:
+
+
+ Потоків завантаження:
+
+
+ Вбудовані зображення
+ Leave a trailing space
+
+
+ Вбудувати смайлики, значки та смайлики за бітси у файл завантаження - щоб потім відтворювати в автономному режимі. Корисно для архівування, розмір файлу буде більшим.
+
+
+ Вбудовані відсутні
+ Leave a trailing space
+
+
+ Вбудовує відсутні смайлики, значки та смайлики за бітси. Вже вбудовані зображення залишаться недоторканими.
+
+
+ В чергу завантаження
+
+
+ В чергу промальовування
+
+
+ В чергу оновлення
+
+
+ ПОМИЛКА:
+ Leave a trailing space
+
+
+ Вхідні аргументи:
+
+
+ Натисніть тут, щоб дізнатися про можливості FFmpeg
+
+
+ Вихідні аргументи:
+
+
+ {fps} {height} {width} {max_int} {save_path}
+ Do not translate
+
+
+ Скинути до типових
+
+
+ FFZ смайлики:
+
+
+ {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} {length} {length_custom=""} {views} {game}
+ Do not translate
+
+
+ Колір шрифту:
+
+
+ Генерувати маску:
+
+
+ Отримати довідку
+
+
+ Сховати кнопку пожертв:
+
+
+ Список ігнорованих користувачів
+ Leave a trailing space
+
+
+ Список імен користувачів - через кому, пробіли навколо ком ігноруються, НЕ враховує регістр.
+
+
+ Не правильне посилання/ІН(ID) кліпу:
+
+
+ Будь ласка, введіть дійсне посилання/ІН(ID) кліпу\nПриклад:\nhttps://clips.twitch.tv/ImportantPlausibleMetalOSsloth\nImportantPlausibleMetalOSsloth
+
+
+ Неправильне вхідне значення
+
+
+ Неправильне значення початку або закінчення
+
+
+ Не правильне Посилання/ІН(ID) відео:
+
+
+ Будь ласка, введіть дійсне посилання/ІН(ID) відео\nПриклад:\nhttps://www.twitch.tv/videos/470741744\n470741744
+
+
+ JSON файл:
+
+
+ Довжина:
+
+
+ Список відео/кліпів(по одному на рядок)
+
+
+ Журнал:
+
+
+ Масове завантаження
+
+
+ OAuth (необов'язково)
+ Leave a trailing space
+
+
+ Потрібен тільки для відео які доступні для підписників. Усі сторонні токени OAuth не працюватимуть. Натисніть, щоб переглянути відео на YouTube про те, як отримати токен.
+
+
+ Офлайн
+ Leave a trailing space
+
+
+ Промальовувати чат, використовуючи тільки ресурси, вбудовані в json-файл чату.
+
+
+ Обрис:
+
+
+ Обмеження паралельних завдань
+
+
+ Часткове промальовування
+
+
+ Якість:
+
+
+ Промальовування
+
+
+ Це тільки для досвідчених користувачів. Якщо ви отримуєте помилку "кінець", це може бути пов'язано з наступними причинами.
+
+
+ Формат файлу:
+
+
+ Кадрова частота:
+
+
+ Кодек:
+
+
+ Кодування
+
+
+ FFmpeg
+
+
+ Основні
+
+
+ Предперегляд
+
+
+ Промальовування
+
+
+ Масштабування
+
+
+ Ширина і висота повинні бути рівними
+
+
+ Замінити вбудовані
+ Leave a trailing space
+
+
+ Замініть у файлі всі вбудовані смайлики, значки та смайлиик за бітси. Всі вбудовані зображення будуть замінені!
+
+
+ Пошук кліпу
+
+
+ Пошук відео
+
+
+ Вибрати усі
+
+
+ Вибрано:
+
+
+ Задати канал
+
+
+ Сортувати:
+
+
+ Готово
+
+
+ Завантаження
+
+
+ ПОМИЛКА
+
+
+ Не працює
+
+
+ Промальовування
+
+
+ Оновлення
+
+
+ Стрімер:
+
+
+ 7TV смайлики:
+
+
+ Повідоалення підписників:
+
+
+ Відмінити
+
+
+ Помилка
+
+
+ Черга завдань
+
+
+ Постачальник смайликів
+ Leave a trailing space
+
+
+ Також вбудуйте у файл смайлики сторонніх розробників. Розмір файлу буде значно більшим.
+
+
+ Формат позначки часу:
+
+
+ Нема
+
+
+ Відносно
+
+
+ Мітки часу:
+
+
+ UTC
+
+
+ Найкращі за весь час
+
+
+ Найкращі за 7 днів
+
+
+ Найкращі за 30 днів
+
+
+ Найкращі за 24 години
+
+
+ Не вдалося знайти мініатюру
+
+
+ Не вдалося отримати інформацію про кліп. Будь ласка, перевірте ІН(ID) кліпу та спробуйте ще раз.
+
+
+ Не вдалося отримати інформацію
+
+
+ Не вдається отримати інформацію про відео/кліп. Будь ласка, перевірте посилання та спробуйте ще раз
+
+
+ Не вдається отримати інформацію про відео. Будь ласка, переконайтеся, що посилання/ІН(ID) правильні та спробуйте ще раз.
+
+
+ Не вдається розібрати вхідні дані
+
+
+ Будь ласка, перевірте правильність введених даних
+
+
+ Не вдалося розібрати посилання
+
+
+ Будь ласка, перевірте посилання відео/кліп
+
+
+ Невідомо
+
+
+ Оновлення
+
+
+ Частота оновлення:
+
+
+ Перелік посилань
+
+
+ Розгорнутий вивід помилки
+
+
+ Розгорнуті помилки:
+
+
+ Створено:
+
+
+ Назва:
+
+
+ Посилання на кліп/відео:
+
+
+ Завантаження відео
+
+
+ Завантаження відео
+
+
+ Термін придатності відео закінчився або ІН(ID) невірний
+
+
+ Посилання/ІН(ID) на відео:
+
+
+ Відео:
+
+
+ Завантажити чат
+
+
+ Тека завантажень:
+
+
+ Завантажити відео
+
+
+ Мова
+ Leave a trailing space
+
+
+ Промальовування чату
+
+
+ Розмір відступів:
+
+
+ Розмір штрихів:
+
+
+ Розмір значків:
+
+
+ Розмір типових смайликів:
+
+
+ Розмір смайликів від постачальників:
+
+
+ Розмір відступів між смайликами:
+
+
+ Розмір відступів:
+
+
+ Розмір вистоти секції:
+
+
+ Розмір вертикального відступу:
+
+
+ Міжрядковий інтервал:
+
+
+ Файл не знайдено:
+ Leave a trailing space
+
+
+ Критична помилка
+
+
+ Тему не знайдено
+
+
+ {theme} не знайдено. Повертаємо тему до системної
+ Do not translate {theme}
+
+
+ Розсіювання
+ Leave a trailing space
+
+
+ У листопаді 2022 року в API Twitch було внесено зміни, завдяки яким повідомлення чату завантажуються лише за цілі секунди. Ця опція використовує додаткові метадані, щоб спробувати відновити повідомлення до моменту, коли вони були фактично надіслані. Це може призвести до зміни порядку повідомлень.
+
+
+ Стиснення:
+
+
+ Немає
+
+
+ Gzip
+ Do not translate
+
+
+ Деякі з включених тем не вдалося записати.
+
+
+ Загальні налаштування
+
+
+ Налаштування черги
+
+
+ Виберіть діапазон промальовування (у секундах)
+
+
+ Список посилань для масового завантаження
+
+
+ Масове завантаження відео
+
+
+ Масове завантаження кліпів
+
+
+ TwitchDownloaderWPF недоступно на вашій рідній мові? Натисніть, щоб дізнатися, як допомогти з перекладом!
+
+
+ Неправильний шлях до папки
+
+
+ Папка не існує
+
+
+ Скасовано
+
+
+ Скасування
+
+
+ Максимальна пропускна здатність
+ Leave a trailing space
+
+
+ Максимальна пропускна здатність, яку дозволяється використовувати новим потокам завантаження, в кілабайтах на секунду.
+
+
+ Різкість:
+
+
+ Локальний
+
+
+ Формат часу:
+
+
+ JSON не обрано
+
+
+ Недостатній доступ. Може знадобитися OAuth.
+
+
+ Постачальник типових смайликів:
+
+
+ Ґуґл
+
+
+ Твіттер
+
+
+ Ніхто
+
+
+ Ви вибрали генерування маски з непрозорим тлом. Зменшіть альфа кольору фону або вимкніть генерацію маски.
+
+
+ Розмір обрису:
+
+
+ crop_start_custom, crop_end_custom, та length_custom форматування базуються на
+
+
+ C# стандартні рядки форматування TimeSpan
+
+
+ Виникла невідома помилка
+
+
+ Завдання не вдалося видалити
+
+
+ Будь ласка, скасуйте завдання або дочекайтеся його завершення, перш ніж видалити
+
+
+ Не вдається завантажити FFmpeg
+
+
+ Не вдалося завантажити FFmpeg. Будь ласка, завантажте його вручну з {0} та розмістіть файл у {1}
+
+
+ Альтернативний колір тла:
+
+
+ Альтернативне тло Leave a trailing space
+
+
+ Змінює колір тла кожного іншого повідомлення чату, щоб допомогти відрізнити їх.
+
+
+ Кодування метаданих:
+
+
+ Помилка
+
+
+ Не вдається запустити переглядач тем програм для Windows. Код помилки: {0}
+
+
+ Відео на сторінку:
+
+
+ Завантаження FFmpeg {0}%
+
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
index 00e51d42..0b8fd66b 100644
--- a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
+++ b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj
@@ -96,6 +96,9 @@
Strings.resx
+
+
+ Strings.resx
Strings.resx
From 81049347757bda3aa8a19115602bc3c5c100515d Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 27 Oct 2023 18:41:11 -0400
Subject: [PATCH 47/50] Fix chat jsons and mp4 metadata lacking any chapters
for VODs with 1 chapter and clips (#875)
* Create GetOrGenerateVideoChapters and update GetVideoInfo query
* Make ChatDownloader & VideoDownloader use GetOrGenerateVideoChapters
* Use the same Game object for Video & Clip info responses
* Generate a bogus chapter for clips
---
TwitchDownloaderCore/ChatDownloader.cs | 18 +++++-
TwitchDownloaderCore/ClipDownloader.cs | 7 ++-
TwitchDownloaderCore/Tools/FfmpegMetadata.cs | 4 +-
TwitchDownloaderCore/TwitchHelper.cs | 55 ++++++++++++++++++-
.../TwitchObjects/Gql/GqlClipResponse.cs | 8 +--
.../TwitchObjects/Gql/GqlVideoResponse.cs | 8 +--
TwitchDownloaderCore/VideoDownloader.cs | 2 +-
7 files changed, 79 insertions(+), 23 deletions(-)
diff --git a/TwitchDownloaderCore/ChatDownloader.cs b/TwitchDownloaderCore/ChatDownloader.cs
index 5ae0da1d..5aabad46 100644
--- a/TwitchDownloaderCore/ChatDownloader.cs
+++ b/TwitchDownloaderCore/ChatDownloader.cs
@@ -288,7 +288,8 @@ public async Task DownloadAsync(IProgress progress, Cancellation
viewCount = videoInfoResponse.data.video.viewCount;
game = videoInfoResponse.data.video.game?.displayName ?? "Unknown";
- GqlVideoChapterResponse videoChapterResponse = await TwitchHelper.GetVideoChapters(int.Parse(videoId));
+ GqlVideoChapterResponse videoChapterResponse = await TwitchHelper.GetOrGenerateVideoChapters(int.Parse(videoId), videoInfoResponse.data.video);
+ chatRoot.video.chapters.EnsureCapacity(videoChapterResponse.data.video.moments.edges.Count);
foreach (var responseChapter in videoChapterResponse.data.video.moments.edges)
{
chatRoot.video.chapters.Add(new VideoChapter
@@ -329,6 +330,21 @@ public async Task DownloadAsync(IProgress progress, Cancellation
viewCount = clipInfoResponse.data.clip.viewCount;
game = clipInfoResponse.data.clip.game?.displayName ?? "Unknown";
connectionCount = 1;
+
+ var clipChapter = TwitchHelper.GenerateClipChapter(clipInfoResponse.data.clip);
+ chatRoot.video.chapters.Add(new VideoChapter
+ {
+ id = clipChapter.node.id,
+ startMilliseconds = clipChapter.node.positionMilliseconds,
+ lengthMilliseconds = clipChapter.node.durationMilliseconds,
+ _type = clipChapter.node._type,
+ description = clipChapter.node.description,
+ subDescription = clipChapter.node.subDescription,
+ thumbnailUrl = clipChapter.node.thumbnailURL,
+ gameId = clipChapter.node.details.game?.id,
+ gameDisplayName = clipChapter.node.details.game?.displayName,
+ gameBoxArtUrl = clipChapter.node.details.game?.boxArtURL
+ });
}
chatRoot.video.id = videoId;
diff --git a/TwitchDownloaderCore/ClipDownloader.cs b/TwitchDownloaderCore/ClipDownloader.cs
index 75884d87..403deb49 100644
--- a/TwitchDownloaderCore/ClipDownloader.cs
+++ b/TwitchDownloaderCore/ClipDownloader.cs
@@ -71,7 +71,8 @@ void DownloadProgressHandler(StreamCopyProgress streamProgress)
_progress.Report(new ProgressReport(ReportType.NewLineStatus, "Encoding Clip Metadata 0%"));
_progress.Report(new ProgressReport(0));
- await EncodeClipWithMetadata(tempFile, downloadOptions.Filename, clipInfo.data.clip, cancellationToken);
+ var clipChapter = TwitchHelper.GenerateClipChapter(clipInfo.data.clip);
+ await EncodeClipWithMetadata(tempFile, downloadOptions.Filename, clipInfo.data.clip, clipChapter, cancellationToken);
_progress.Report(new ProgressReport(ReportType.SameLineStatus, "Encoding Clip Metadata 100%"));
_progress.Report(new ProgressReport(100));
@@ -137,14 +138,14 @@ private static async Task DownloadFileTaskAsync(string url, string destinationFi
}
}
- private async Task EncodeClipWithMetadata(string inputFile, string destinationFile, Clip clipMetadata, CancellationToken cancellationToken)
+ private async Task EncodeClipWithMetadata(string inputFile, string destinationFile, Clip clipMetadata, VideoMomentEdge clipChapter, CancellationToken cancellationToken)
{
var metadataFile = $"{Path.GetFileName(inputFile)}_metadata.txt";
try
{
await FfmpegMetadata.SerializeAsync(metadataFile, clipMetadata.broadcaster.displayName, downloadOptions.Id, clipMetadata.title, clipMetadata.createdAt, clipMetadata.viewCount,
- cancellationToken: cancellationToken);
+ videoMomentEdges: new[] { clipChapter }, cancellationToken: cancellationToken);
var process = new Process
{
diff --git a/TwitchDownloaderCore/Tools/FfmpegMetadata.cs b/TwitchDownloaderCore/Tools/FfmpegMetadata.cs
index 2e7959b8..e3ce2107 100644
--- a/TwitchDownloaderCore/Tools/FfmpegMetadata.cs
+++ b/TwitchDownloaderCore/Tools/FfmpegMetadata.cs
@@ -14,7 +14,7 @@ public static class FfmpegMetadata
private const string LINE_FEED = "\u000A";
public static async Task SerializeAsync(string filePath, string streamerName, string videoId, string videoTitle, DateTime videoCreation, int viewCount, string videoDescription = null,
- double startOffsetSeconds = 0, List videoMomentEdges = null, CancellationToken cancellationToken = default)
+ double startOffsetSeconds = 0, IEnumerable videoMomentEdges = null, CancellationToken cancellationToken = default)
{
await using var fs = new FileStream(filePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None);
await using var sw = new StreamWriter(fs) { NewLine = LINE_FEED };
@@ -43,7 +43,7 @@ private static async Task SerializeGlobalMetadata(StreamWriter sw, string stream
await sw.WriteLineAsync(@$"Views: {viewCount}");
}
- private static async Task SerializeChapters(StreamWriter sw, List videoMomentEdges, double startOffsetSeconds)
+ private static async Task SerializeChapters(StreamWriter sw, IEnumerable videoMomentEdges, double startOffsetSeconds)
{
if (videoMomentEdges is null)
{
diff --git a/TwitchDownloaderCore/TwitchHelper.cs b/TwitchDownloaderCore/TwitchHelper.cs
index bdd0db78..54d5b372 100644
--- a/TwitchDownloaderCore/TwitchHelper.cs
+++ b/TwitchDownloaderCore/TwitchHelper.cs
@@ -30,7 +30,7 @@ public static async Task GetVideoInfo(int videoId)
{
RequestUri = new Uri("https://gql.twitch.tv/gql"),
Method = HttpMethod.Post,
- Content = new StringContent("{\"query\":\"query{video(id:\\\"" + videoId + "\\\"){title,thumbnailURLs(height:180,width:320),createdAt,lengthSeconds,owner{id,displayName},viewCount,game{id,displayName},description}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
+ Content = new StringContent("{\"query\":\"query{video(id:\\\"" + videoId + "\\\"){title,thumbnailURLs(height:180,width:320),createdAt,lengthSeconds,owner{id,displayName},viewCount,game{id,displayName,boxArtURL},description}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
};
request.Headers.Add("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko");
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
@@ -72,7 +72,7 @@ public static async Task GetClipInfo(object clipId)
{
RequestUri = new Uri("https://gql.twitch.tv/gql"),
Method = HttpMethod.Post,
- Content = new StringContent("{\"query\":\"query{clip(slug:\\\"" + clipId + "\\\"){title,thumbnailURL,createdAt,durationSeconds,broadcaster{id,displayName},videoOffsetSeconds,video{id},viewCount,game{id,displayName}}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
+ Content = new StringContent("{\"query\":\"query{clip(slug:\\\"" + clipId + "\\\"){title,thumbnailURL,createdAt,durationSeconds,broadcaster{id,displayName},videoOffsetSeconds,video{id},viewCount,game{id,displayName,boxArtURL}}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
};
request.Headers.Add("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko");
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
@@ -912,6 +912,7 @@ public static async Task GetUserInfo(List idList)
return imageBytes;
}
+ /// When a given video has only 1 chapter, data.video.moments.edges will be empty.
public static async Task GetVideoChapters(int videoId)
{
var request = new HttpRequestMessage()
@@ -925,5 +926,55 @@ public static async Task GetVideoChapters(int videoId)
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync();
}
+
+ public static async Task GetOrGenerateVideoChapters(int videoId, VideoInfo videoInfo)
+ {
+ var chapterResponse = await GetVideoChapters(videoId);
+
+ // Video has only 1 chapter, generate a bogus video chapter with the information we have available.
+ if (chapterResponse.data.video.moments.edges.Count == 0)
+ {
+ chapterResponse.data.video.moments.edges.Add(
+ GenerateVideoMomentEdge(0, videoInfo.lengthSeconds, videoInfo.game?.id, videoInfo.game?.displayName, videoInfo.game?.displayName, videoInfo.game?.boxArtURL
+ ));
+ }
+
+ return chapterResponse;
+ }
+
+ public static VideoMomentEdge GenerateClipChapter(Clip clipInfo)
+ {
+ return GenerateVideoMomentEdge(0, clipInfo.durationSeconds, clipInfo.game?.id, clipInfo.game?.displayName, clipInfo.game?.displayName, clipInfo.game?.boxArtURL);
+ }
+
+ private static VideoMomentEdge GenerateVideoMomentEdge(int startSeconds, int lengthSeconds, string gameId = null, string gameDisplayName = null, string gameDescription = null, string gameBoxArtUrl = null)
+ {
+ gameId ??= "-1";
+ gameDisplayName ??= "Unknown";
+ gameDescription ??= "Unknown";
+ gameBoxArtUrl ??= "";
+
+ return new VideoMomentEdge
+ {
+ node = new VideoMoment
+ {
+ id = "",
+ _type = "GAME_CHANGE",
+ positionMilliseconds = startSeconds,
+ durationMilliseconds = lengthSeconds * 1000,
+ description = gameDescription,
+ subDescription = "",
+ details = new GameChangeMomentDetails
+ {
+ game = new Game
+ {
+ id = gameId,
+ displayName = gameDisplayName,
+ boxArtURL = gameBoxArtUrl.Replace("{width}", "40").Replace("{height}", "53")
+ }
+ }
+ }
+ };
+ }
}
}
\ No newline at end of file
diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipResponse.cs
index 0ee116c8..8083af9c 100644
--- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipResponse.cs
+++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipResponse.cs
@@ -13,12 +13,6 @@ public class ClipVideo
public string id { get; set; }
}
- public class ClipGame
- {
- public string id { get; set; }
- public string displayName { get; set; }
- }
-
public class Clip
{
public string title { get; set; }
@@ -29,7 +23,7 @@ public class Clip
public int? videoOffsetSeconds { get; set; }
public ClipVideo video { get; set; }
public int viewCount { get; set; }
- public ClipGame game { get; set; }
+ public Game game { get; set; }
}
public class ClipData
diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
index 986857c9..9b0856a2 100644
--- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
+++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
@@ -9,12 +9,6 @@ public class VideoOwner
public string displayName { get; set; }
}
- public class VideoGame
- {
- public string id { get; set; }
- public string displayName { get; set; }
- }
-
public class VideoInfo
{
public string title { get; set; }
@@ -23,7 +17,7 @@ public class VideoInfo
public int lengthSeconds { get; set; }
public VideoOwner owner { get; set; }
public int viewCount { get; set; }
- public VideoGame game { get; set; }
+ public Game game { get; set; }
///
/// Some values, such as newlines, are repeated twice for some reason.
/// This can be filtered out with: description?.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd()
diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs
index 0135dfad..bd604ff2 100644
--- a/TwitchDownloaderCore/VideoDownloader.cs
+++ b/TwitchDownloaderCore/VideoDownloader.cs
@@ -53,7 +53,7 @@ public async Task DownloadAsync(CancellationToken cancellationToken)
throw new NullReferenceException("Invalid VOD, deleted/expired VOD possibly?");
}
- GqlVideoChapterResponse videoChapterResponse = await TwitchHelper.GetVideoChapters(downloadOptions.Id);
+ GqlVideoChapterResponse videoChapterResponse = await TwitchHelper.GetOrGenerateVideoChapters(downloadOptions.Id, videoInfoResponse.data.video);
var (playlistUrl, bandwidth) = await GetPlaylistUrl();
var baseUrl = new Uri(playlistUrl[..(playlistUrl.LastIndexOf('/') + 1)], UriKind.Absolute);
From 2d64a7d2ecb078b48e43918185b43278584e45c0 Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 27 Oct 2023 22:12:57 -0400
Subject: [PATCH 48/50] Add context menu to video/clip search mass downloaders
(#876)
* Add context menu to mass downloaders
* Update TaskData
* Update translations
---
.../Translations/Strings.Designer.cs | 28 +++++++++++++
.../Translations/Strings.es.resx | 11 ++++-
.../Translations/Strings.fr.resx | 9 ++++
.../Translations/Strings.pl.resx | 9 ++++
TwitchDownloaderWPF/Translations/Strings.resx | 9 ++++
.../Translations/Strings.ru.resx | 9 ++++
.../Translations/Strings.tr.resx | 11 ++++-
.../Translations/Strings.uk.resx | 9 ++++
.../Translations/Strings.zh.resx | 9 ++++
TwitchDownloaderWPF/TwitchTasks/TaskData.cs | 2 +-
TwitchDownloaderWPF/WindowMassDownload.xaml | 22 +++++++++-
.../WindowMassDownload.xaml.cs | 41 ++++++++++++++++++-
12 files changed, 164 insertions(+), 5 deletions(-)
diff --git a/TwitchDownloaderWPF/Translations/Strings.Designer.cs b/TwitchDownloaderWPF/Translations/Strings.Designer.cs
index 0ddc9a35..2680bae6 100644
--- a/TwitchDownloaderWPF/Translations/Strings.Designer.cs
+++ b/TwitchDownloaderWPF/Translations/Strings.Designer.cs
@@ -1,6 +1,7 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -419,6 +420,24 @@ public static string ClipLinkId {
}
}
+ ///
+ /// Looks up a localized string similar to Copy ID to clipboard.
+ ///
+ public static string CopyVideoIDToClipboard {
+ get {
+ return ResourceManager.GetString("CopyVideoIDToClipboard", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Copy URL to clipboard.
+ ///
+ public static string CopyVideoURLToClipboard {
+ get {
+ return ResourceManager.GetString("CopyVideoURLToClipboard", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to End.
///
@@ -1085,6 +1104,15 @@ public static string OfflineTooltip {
}
}
+ ///
+ /// Looks up a localized string similar to Open in browser.
+ ///
+ public static string OpenVideoInBrowser {
+ get {
+ return ResourceManager.GetString("OpenVideoInBrowser", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Outline:.
///
diff --git a/TwitchDownloaderWPF/Translations/Strings.es.resx b/TwitchDownloaderWPF/Translations/Strings.es.resx
index 6db5706a..d40389e1 100644
--- a/TwitchDownloaderWPF/Translations/Strings.es.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.es.resx
@@ -778,4 +778,13 @@
Descarga FFmpeg {0}%
-
+
+ Copy ID to clipboard
+
+
+ Copy URL to clipboard
+
+
+ Open in browser
+
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.fr.resx b/TwitchDownloaderWPF/Translations/Strings.fr.resx
index f7a00758..6be5233c 100644
--- a/TwitchDownloaderWPF/Translations/Strings.fr.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.fr.resx
@@ -777,4 +777,13 @@
Downloading FFmpeg {0}%
+
+ Copier l'identifiant vidéo
+
+
+ Copier le lien vidéo
+
+
+ Ouvrir dans un navigateur web
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.pl.resx b/TwitchDownloaderWPF/Translations/Strings.pl.resx
index f9cb43b9..858d3e5d 100644
--- a/TwitchDownloaderWPF/Translations/Strings.pl.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.pl.resx
@@ -777,4 +777,13 @@
Downloading FFmpeg {0}%
+
+ Copy ID to clipboard
+
+
+ Copy URL to clipboard
+
+
+ Open in browser
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.resx b/TwitchDownloaderWPF/Translations/Strings.resx
index 0bc72b6b..1f22a7b8 100644
--- a/TwitchDownloaderWPF/Translations/Strings.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.resx
@@ -776,4 +776,13 @@
Downloading FFmpeg {0}%
+
+ Copy ID to clipboard
+
+
+ Copy URL to clipboard
+
+
+ Open in browser
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.ru.resx b/TwitchDownloaderWPF/Translations/Strings.ru.resx
index 454118e6..a627640d 100644
--- a/TwitchDownloaderWPF/Translations/Strings.ru.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.ru.resx
@@ -777,4 +777,13 @@
Downloading FFmpeg {0}%
+
+ Copy ID to clipboard
+
+
+ Copy URL to clipboard
+
+
+ Open in browser
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.tr.resx b/TwitchDownloaderWPF/Translations/Strings.tr.resx
index e4acd7b5..a2b08a71 100644
--- a/TwitchDownloaderWPF/Translations/Strings.tr.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.tr.resx
@@ -778,4 +778,13 @@
FFmpeg İndiriliyor {0}%
-
+
+ Copy ID to clipboard
+
+
+ Copy URL to clipboard
+
+
+ Open in browser
+
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.uk.resx b/TwitchDownloaderWPF/Translations/Strings.uk.resx
index 98839027..d7bce07f 100644
--- a/TwitchDownloaderWPF/Translations/Strings.uk.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.uk.resx
@@ -777,4 +777,13 @@
Завантаження FFmpeg {0}%
+
+ Copy ID to clipboard
+
+
+ Copy URL to clipboard
+
+
+ Open in browser
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/Translations/Strings.zh.resx b/TwitchDownloaderWPF/Translations/Strings.zh.resx
index 1073660f..fe1a87c8 100644
--- a/TwitchDownloaderWPF/Translations/Strings.zh.resx
+++ b/TwitchDownloaderWPF/Translations/Strings.zh.resx
@@ -776,4 +776,13 @@
Downloading FFmpeg {0}%
+
+ Copy ID to clipboard
+
+
+ Copy URL to clipboard
+
+
+ Open in browser
+
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/TwitchTasks/TaskData.cs b/TwitchDownloaderWPF/TwitchTasks/TaskData.cs
index 29f89b0b..5d2c8872 100644
--- a/TwitchDownloaderWPF/TwitchTasks/TaskData.cs
+++ b/TwitchDownloaderWPF/TwitchTasks/TaskData.cs
@@ -28,7 +28,7 @@ public string LengthFormatted
return $"{time.Minutes:D2}:{time.Seconds:D2}";
}
- return $"{time.Seconds:D2}s";
+ return $"{time.Seconds:D1}s";
}
}
}
diff --git a/TwitchDownloaderWPF/WindowMassDownload.xaml b/TwitchDownloaderWPF/WindowMassDownload.xaml
index 8be36e6b..53cf01f3 100644
--- a/TwitchDownloaderWPF/WindowMassDownload.xaml
+++ b/TwitchDownloaderWPF/WindowMassDownload.xaml
@@ -5,6 +5,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TwitchDownloaderWPF"
xmlns:behave="clr-namespace:TwitchDownloaderWPF.Behaviors"
+ xmlns:fa="http://schemas.fontawesome.com/icons/"
xmlns:lex="http://wpflocalizeextension.codeplex.com"
lex:LocalizeDictionary.DesignCulture=""
lex:ResxLocalizationProvider.DefaultAssembly="TwitchDownloaderWPF"
@@ -52,7 +53,26 @@
-
+
+
+
+
+
+
+
+
diff --git a/TwitchDownloaderWPF/WindowMassDownload.xaml.cs b/TwitchDownloaderWPF/WindowMassDownload.xaml.cs
index 02424b58..7c6e9514 100644
--- a/TwitchDownloaderWPF/WindowMassDownload.xaml.cs
+++ b/TwitchDownloaderWPF/WindowMassDownload.xaml.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
@@ -160,7 +161,7 @@ private async Task UpdateList()
if (StatusImage != null) StatusImage.Visibility = Visibility.Hidden;
}
- private void Border_MouseUp(object sender, MouseButtonEventArgs e)
+ private void Border_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (sender is not Border border) return;
if (border.DataContext is not TaskData taskData) return;
@@ -271,5 +272,43 @@ private async void ComboVideoCount_SelectionChanged(object sender, SelectionChan
cursorIndex = -1;
await UpdateList();
}
+
+ private void MenuItemCopyVideoID_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MenuItem { DataContext: TaskData taskData }) return;
+
+ var id = taskData.Id;
+ Clipboard.SetText(id);
+
+ e.Handled = true;
+ }
+
+ private void MenuItemCopyVideoUrl_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MenuItem { DataContext: TaskData taskData }) return;
+
+ var id = taskData.Id;
+ var url = id.All(char.IsDigit)
+ ? $"https://twitch.tv/videos/{id}"
+ : $"https://clips.twitch.tv/{id}";
+
+ Clipboard.SetText(url);
+
+ e.Handled = true;
+ }
+
+ private void MenuItemOpenInBrowser_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MenuItem { DataContext: TaskData taskData }) return;
+
+ var id = taskData.Id;
+ var url = id.All(char.IsDigit)
+ ? $"https://twitch.tv/videos/{id}"
+ : $"https://clips.twitch.tv/{id}";
+
+ Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
+
+ e.Handled = true;
+ }
}
}
From c3db4a67463c7acb11639c932296df23cdd1854c Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 27 Oct 2023 23:22:51 -0400
Subject: [PATCH 49/50] Deserialize chat json files without relying on the file
extension (#858)
* Deserialize chat json files without relying on the file extension
This fixes a huge issue with the chat updater that somehow went unnoticed
This also enables deserializing UTF16 BOM and UTF32 BOM files
* Thanks Rider
---
TwitchDownloaderCore/Chat/ChatJson.cs | 89 +++++++++++++++++++++------
1 file changed, 69 insertions(+), 20 deletions(-)
diff --git a/TwitchDownloaderCore/Chat/ChatJson.cs b/TwitchDownloaderCore/Chat/ChatJson.cs
index 6e04b41f..df0de55e 100644
--- a/TwitchDownloaderCore/Chat/ChatJson.cs
+++ b/TwitchDownloaderCore/Chat/ChatJson.cs
@@ -1,9 +1,10 @@
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
-using System.Runtime.Serialization;
+using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
@@ -44,20 +45,9 @@ public static class ChatJson
AllowTrailingCommas = true
};
- await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
- switch (Path.GetExtension(filePath).ToLower())
+ await using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
- case ".gz":
- await using (var gs = new GZipStream(fs, CompressionMode.Decompress))
- {
- jsonDocument = await JsonDocument.ParseAsync(gs, deserializationOptions, cancellationToken);
- }
- break;
- case ".json":
- jsonDocument = await JsonDocument.ParseAsync(fs, deserializationOptions, cancellationToken);
- break;
- default:
- throw new NotSupportedException(Path.GetFileName(filePath) + " is not a valid chat format");
+ jsonDocument = await GetJsonDocumentAsync(fs, filePath, deserializationOptions, cancellationToken);
}
if (jsonDocument.RootElement.TryGetProperty("FileInfo", out JsonElement fileInfoElement))
@@ -131,7 +121,66 @@ public static class ChatJson
return returnChatRoot;
}
- private static async ValueTask UpgradeChatJson(ChatRoot chatRoot)
+ private static async Task GetJsonDocumentAsync(Stream stream, string filePath, JsonDocumentOptions deserializationOptions, CancellationToken cancellationToken = default)
+ {
+ if (!stream.CanSeek)
+ {
+ // We aren't able to verify the file type. Pretend it's JSON.
+ return await JsonDocument.ParseAsync(stream, deserializationOptions, cancellationToken);
+ }
+
+ const int RENT_LENGTH = 4;
+ var rentedBuffer = ArrayPool.Shared.Rent(RENT_LENGTH);
+ try
+ {
+ if (await stream.ReadAsync(rentedBuffer.AsMemory(0, RENT_LENGTH), cancellationToken) != RENT_LENGTH)
+ {
+ throw new EndOfStreamException($"{Path.GetFileName(filePath)} is not a valid chat format.");
+ }
+
+ stream.Seek(-RENT_LENGTH, SeekOrigin.Current);
+
+ // TODO: use list patterns when .NET 7+
+ // https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
+ switch (rentedBuffer[0], rentedBuffer[1], rentedBuffer[2], rentedBuffer[3])
+ {
+ case (0x1F, 0x8B, _, _): // https://docs.fileformat.com/compression/gz/#gz-file-header
+ {
+ await using var gs = new GZipStream(stream, CompressionMode.Decompress);
+ return await GetJsonDocumentAsync(gs, filePath, deserializationOptions, cancellationToken);
+ }
+ case (0x00, 0x00, 0xFE, 0xFF): // UTF-32 BE
+ case (0xFF, 0xFE, 0x00, 0x00): // UTF-32 LE
+ {
+ using var sr = new StreamReader(stream, Encoding.UTF32);
+ var jsonString = await sr.ReadToEndAsync();
+ return JsonDocument.Parse(jsonString.AsMemory(), deserializationOptions);
+ }
+ case (0xFE, 0xFF, _, _): // UTF-16 BE
+ case (0xFF, 0xFE, _, _): // UTF-16 LE
+ {
+ using var sr = new StreamReader(stream, Encoding.Unicode);
+ var jsonString = await sr.ReadToEndAsync();
+ return JsonDocument.Parse(jsonString.AsMemory(), deserializationOptions);
+ }
+ case (0xEF, 0xBB, 0xBF, _): // UTF-8
+ case ((byte)'{', _, _, _): // Starts with a '{', probably JSON
+ {
+ return await JsonDocument.ParseAsync(stream, deserializationOptions, cancellationToken);
+ }
+ default:
+ {
+ throw new NotSupportedException($"{Path.GetFileName(filePath)} is not a valid chat format.");
+ }
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(rentedBuffer);
+ }
+ }
+
+ private static async Task UpgradeChatJson(ChatRoot chatRoot)
{
const int MAX_STREAM_LENGTH = 172_800; // 48 hours in seconds. https://help.twitch.tv/s/article/broadcast-guidelines
chatRoot.video ??= new Video
@@ -195,14 +244,14 @@ public static async Task SerializeAsync(string filePath, ChatRoot chatRoot, Chat
await JsonSerializer.SerializeAsync(fs, chatRoot, _jsonSerializerOptions, cancellationToken);
break;
case ChatCompression.Gzip:
- await using (var gs = new GZipStream(fs, CompressionLevel.SmallestSize))
- {
- await JsonSerializer.SerializeAsync(gs, chatRoot, _jsonSerializerOptions, cancellationToken);
- }
+ {
+ await using var gs = new GZipStream(fs, CompressionLevel.SmallestSize);
+ await JsonSerializer.SerializeAsync(gs, chatRoot, _jsonSerializerOptions, cancellationToken);
break;
+ }
default:
throw new NotSupportedException($"{compression} is not a supported chat compression.");
}
}
}
-}
+}
\ No newline at end of file
From 469336f938a4f6b941199dca6583615ca0ee161f Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Fri, 27 Oct 2023 23:33:55 -0400
Subject: [PATCH 50/50] Many chat updater fixes (#859)
* Fix incorrect percentages in chat updater
* Make crop lock object not static
* Update video info if possible when updating chats
* Compress chat crop updater temp files with gzip
* Fix ArgumentOutOfRangeException when loading information from chat files with less than 2 comments
* Add functionality to deserialize only the first and last comments
* Fix chapter updating
---
TwitchDownloaderCore/Chat/ChatJson.cs | 6 +-
TwitchDownloaderCore/ChatRenderer.cs | 2 +-
TwitchDownloaderCore/ChatUpdater.cs | 114 +++++++++++++++---
.../Tools/JsonElementExtensions.cs | 32 +++++
TwitchDownloaderWPF/PageChatUpdate.xaml.cs | 3 +-
5 files changed, 137 insertions(+), 20 deletions(-)
create mode 100644 TwitchDownloaderCore/Tools/JsonElementExtensions.cs
diff --git a/TwitchDownloaderCore/Chat/ChatJson.cs b/TwitchDownloaderCore/Chat/ChatJson.cs
index df0de55e..b156a184 100644
--- a/TwitchDownloaderCore/Chat/ChatJson.cs
+++ b/TwitchDownloaderCore/Chat/ChatJson.cs
@@ -29,7 +29,7 @@ public static class ChatJson
/// A representation the deserialized chat json file.
/// The file does not exist.
/// The file is not a valid chat format.
- public static async Task DeserializeAsync(string filePath, bool getComments = true, bool getEmbeds = true, CancellationToken cancellationToken = new())
+ public static async Task DeserializeAsync(string filePath, bool getComments = true, bool onlyFirstAndLastComments = false, bool getEmbeds = true, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(filePath, nameof(filePath));
@@ -82,7 +82,9 @@ public static class ChatJson
{
if (jsonDocument.RootElement.TryGetProperty("comments", out JsonElement commentsElement))
{
- returnChatRoot.comments = commentsElement.Deserialize>(options: _jsonSerializerOptions);
+ returnChatRoot.comments = onlyFirstAndLastComments
+ ? commentsElement.DeserializeFirstAndLastFromList(options: _jsonSerializerOptions)
+ : commentsElement.Deserialize>(options: _jsonSerializerOptions);
}
}
diff --git a/TwitchDownloaderCore/ChatRenderer.cs b/TwitchDownloaderCore/ChatRenderer.cs
index 1516082e..d9bb9276 100644
--- a/TwitchDownloaderCore/ChatRenderer.cs
+++ b/TwitchDownloaderCore/ChatRenderer.cs
@@ -1694,7 +1694,7 @@ private static bool IsRightToLeft(ReadOnlySpan message)
public async Task ParseJsonAsync(CancellationToken cancellationToken = new())
{
- chatRoot = await ChatJson.DeserializeAsync(renderOptions.InputFile, true, true, cancellationToken);
+ chatRoot = await ChatJson.DeserializeAsync(renderOptions.InputFile, true, false, true, cancellationToken);
return chatRoot;
}
diff --git a/TwitchDownloaderCore/ChatUpdater.cs b/TwitchDownloaderCore/ChatUpdater.cs
index 2fb1b315..5e1dd01f 100644
--- a/TwitchDownloaderCore/ChatUpdater.cs
+++ b/TwitchDownloaderCore/ChatUpdater.cs
@@ -8,12 +8,14 @@
using TwitchDownloaderCore.Options;
using TwitchDownloaderCore.Tools;
using TwitchDownloaderCore.TwitchObjects;
+using TwitchDownloaderCore.TwitchObjects.Gql;
namespace TwitchDownloaderCore
{
public sealed class ChatUpdater
{
public ChatRoot chatRoot { get; internal set; } = new();
+ private readonly object _cropChatRootLock = new();
private readonly ChatUpdateOptions _updateOptions;
@@ -25,11 +27,6 @@ public ChatUpdater(ChatUpdateOptions updateOptions)
"TwitchDownloader");
}
- private static class SharedObjects
- {
- internal static object CropChatRootLock = new();
- }
-
public async Task UpdateAsync(IProgress progress, CancellationToken cancellationToken)
{
chatRoot.FileInfo = new() { Version = ChatRootVersion.CurrentVersion, CreatedAt = chatRoot.FileInfo.CreatedAt, UpdatedAt = DateTime.Now };
@@ -40,10 +37,13 @@ public async Task UpdateAsync(IProgress progress, CancellationTo
// Dynamic step count setup
int currentStep = 0;
- int totalSteps = 1;
+ int totalSteps = 2;
if (_updateOptions.CropBeginning || _updateOptions.CropEnding) totalSteps++;
if (_updateOptions.EmbedMissing || _updateOptions.ReplaceEmbeds) totalSteps++;
+ currentStep++;
+ await UpdateVideoInfo(totalSteps, currentStep, progress, cancellationToken);
+
// If we are editing the chat crop
if (_updateOptions.CropBeginning || _updateOptions.CropEnding)
{
@@ -60,7 +60,7 @@ public async Task UpdateAsync(IProgress progress, CancellationTo
// Finally save the output to file!
progress.Report(new ProgressReport(ReportType.NewLineStatus, $"Writing Output File [{++currentStep}/{totalSteps}]"));
- progress.Report(new ProgressReport(totalSteps / currentStep));
+ progress.Report(new ProgressReport(currentStep * 100 / totalSteps));
switch (_updateOptions.OutputFormat)
{
@@ -78,17 +78,100 @@ public async Task UpdateAsync(IProgress progress, CancellationTo
}
}
+ private async Task UpdateVideoInfo(int totalSteps, int currentStep, IProgress progress, CancellationToken cancellationToken)
+ {
+ progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Updating Video Info [{currentStep}/{totalSteps}]"));
+ progress.Report(new ProgressReport(currentStep * 100 / totalSteps));
+
+ if (chatRoot.video.id.All(char.IsDigit))
+ {
+ var videoId = int.Parse(chatRoot.video.id);
+ VideoInfo videoInfo = null;
+ try
+ {
+ videoInfo = (await TwitchHelper.GetVideoInfo(videoId)).data.video;
+ }
+ catch { /* Eat the exception */ }
+
+ if (videoInfo is null)
+ {
+ progress.Report(new ProgressReport(ReportType.SameLineStatus, "Unable to fetch video info, deleted/expired VOD possibly?"));
+ return;
+ }
+
+ chatRoot.video.title = videoInfo.title;
+ chatRoot.video.description = videoInfo.description;
+ chatRoot.video.created_at = videoInfo.createdAt;
+ chatRoot.video.length = videoInfo.lengthSeconds;
+ chatRoot.video.viewCount = videoInfo.viewCount;
+ chatRoot.video.game = videoInfo.game.displayName;
+
+ var chaptersInfo = (await TwitchHelper.GetOrGenerateVideoChapters(videoId, videoInfo)).data.video.moments.edges;
+ foreach (var responseChapter in chaptersInfo)
+ {
+ chatRoot.video.chapters.Add(new VideoChapter
+ {
+ id = responseChapter.node.id,
+ startMilliseconds = responseChapter.node.positionMilliseconds,
+ lengthMilliseconds = responseChapter.node.durationMilliseconds,
+ _type = responseChapter.node._type,
+ description = responseChapter.node.description,
+ subDescription = responseChapter.node.subDescription,
+ thumbnailUrl = responseChapter.node.thumbnailURL,
+ gameId = responseChapter.node.details.game?.id,
+ gameDisplayName = responseChapter.node.details.game?.displayName,
+ gameBoxArtUrl = responseChapter.node.details.game?.boxArtURL
+ });
+ }
+ }
+ else
+ {
+ var clipId = chatRoot.video.id;
+ Clip clipInfo = null;
+ try
+ {
+ clipInfo = (await TwitchHelper.GetClipInfo(clipId)).data.clip;
+ }
+ catch { /* Eat the exception */ }
+
+ if (clipInfo is null)
+ {
+ progress.Report(new ProgressReport(ReportType.SameLineStatus, "Unable to fetch clip info, deleted possibly?"));
+ return;
+ }
+
+ chatRoot.video.title = clipInfo.title;
+ chatRoot.video.created_at = clipInfo.createdAt;
+ chatRoot.video.length = clipInfo.durationSeconds;
+ chatRoot.video.viewCount = clipInfo.viewCount;
+ chatRoot.video.game = clipInfo.game.displayName;
+
+ var clipChapter = TwitchHelper.GenerateClipChapter(clipInfo);
+ chatRoot.video.chapters.Add(new VideoChapter
+ {
+ id = clipChapter.node.id,
+ startMilliseconds = clipChapter.node.positionMilliseconds,
+ lengthMilliseconds = clipChapter.node.durationMilliseconds,
+ _type = clipChapter.node._type,
+ description = clipChapter.node.description,
+ subDescription = clipChapter.node.subDescription,
+ thumbnailUrl = clipChapter.node.thumbnailURL,
+ gameId = clipChapter.node.details.game?.id,
+ gameDisplayName = clipChapter.node.details.game?.displayName,
+ gameBoxArtUrl = clipChapter.node.details.game?.boxArtURL
+ });
+ }
+ }
+
private async Task UpdateChatCrop(int totalSteps, int currentStep, IProgress progress, CancellationToken cancellationToken)
{
progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Updating Chat Crop [{currentStep}/{totalSteps}]"));
- progress.Report(new ProgressReport(totalSteps / currentStep));
-
- chatRoot.video ??= new Video();
+ progress.Report(new ProgressReport(currentStep * 100 / totalSteps));
bool cropTaskVodExpired = false;
var cropTaskProgress = new Progress(report =>
{
- if (((string)report.Data).ToLower().Contains("vod is expired"))
+ 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)
@@ -145,7 +228,7 @@ private async Task UpdateChatCrop(int totalSteps, int currentStep, IProgress progress, CancellationToken cancellationToken)
{
progress.Report(new ProgressReport(ReportType.NewLineStatus, $"Updating Embeds [{currentStep}/{totalSteps}]"));
- progress.Report(new ProgressReport(totalSteps / currentStep));
+ progress.Report(new ProgressReport(currentStep * 100 / totalSteps));
chatRoot.embeddedData ??= new EmbeddedData();
@@ -313,7 +396,7 @@ private async Task ChatEndingCropTask(IProgress progress, Cancel
ChatDownloader chatDownloader = new ChatDownloader(downloadOptions);
await chatDownloader.DownloadAsync(new Progress(), cancellationToken);
- ChatRoot newChatRoot = await ChatJson.DeserializeAsync(inputFile, getComments: true, getEmbeds: false, cancellationToken);
+ ChatRoot newChatRoot = await ChatJson.DeserializeAsync(inputFile, getComments: true, onlyFirstAndLastComments: false, getEmbeds: false, cancellationToken);
// Append the new comment section
SortedSet commentsSet = new SortedSet(new SortedCommentComparer());
@@ -325,7 +408,7 @@ private async Task ChatEndingCropTask(IProgress progress, Cancel
}
}
- lock (SharedObjects.CropChatRootLock)
+ lock (_cropChatRootLock)
{
foreach (var comment in chatRoot.comments)
{
@@ -345,6 +428,7 @@ private ChatDownloadOptions GetCropDownloadOptions(string videoId, string tempFi
{
Id = videoId,
DownloadFormat = ChatFormat.Json, // json is required to parse as a new chatroot object
+ Compression = ChatCompression.Gzip,
Filename = tempFile,
CropBeginning = true,
CropBeginningTime = sectionStart,
@@ -361,7 +445,7 @@ private ChatDownloadOptions GetCropDownloadOptions(string videoId, string tempFi
public async Task ParseJsonAsync(CancellationToken cancellationToken = new())
{
- chatRoot = await ChatJson.DeserializeAsync(_updateOptions.InputFile, true, true, cancellationToken);
+ chatRoot = await ChatJson.DeserializeAsync(_updateOptions.InputFile, true, false, true, cancellationToken);
return chatRoot;
}
}
diff --git a/TwitchDownloaderCore/Tools/JsonElementExtensions.cs b/TwitchDownloaderCore/Tools/JsonElementExtensions.cs
new file mode 100644
index 00000000..24f0e82e
--- /dev/null
+++ b/TwitchDownloaderCore/Tools/JsonElementExtensions.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Text.Json;
+
+namespace TwitchDownloaderCore.Tools
+{
+ public static class JsonElementExtensions
+ {
+ public static List DeserializeFirstAndLastFromList(this JsonElement arrayElement, JsonSerializerOptions options = null)
+ {
+ // It's not the prettiest, but for arrays with thousands of objects it can save whole seconds and prevent tons of fragmented memory
+ var list = new List(2);
+ JsonElement lastElement = default;
+ foreach (var element in arrayElement.EnumerateArray())
+ {
+ if (list.Count == 0)
+ {
+ list.Add(element.Deserialize(options: options));
+ continue;
+ }
+
+ lastElement = element;
+ }
+
+ if (lastElement.ValueKind != JsonValueKind.Undefined)
+ {
+ list.Add(lastElement.Deserialize(options: options));
+ }
+
+ return list;
+ }
+ }
+}
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
index 0acbd49e..706ac3f3 100644
--- a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
+++ b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
@@ -66,8 +66,7 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e)
try
{
- ChatJsonInfo = await ChatJson.DeserializeAsync(InputFile, true, false, CancellationToken.None);
- ChatJsonInfo.comments.RemoveRange(1, ChatJsonInfo.comments.Count - 2);
+ ChatJsonInfo = await ChatJson.DeserializeAsync(InputFile, true, true, false, CancellationToken.None);
GC.Collect();
}
catch (Exception ex)