diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 7f00e89a..4ec706b3 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -18,7 +18,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore Dependencies run: dotnet restore TwitchDownloaderWPF - name: Build Windows GUI @@ -35,12 +35,12 @@ jobs: file-name: ffmpeg.zip - name: Bundle FFmpeg - run: tar xfz ffmpeg.zip --strip-components=1; copy bin/ffmpeg.exe TwitchDownloaderWPF/bin/Release/net6.0-windows/publish/win-x64/ffmpeg.exe + run: tar xfz ffmpeg.zip --strip-components=1; copy bin/ffmpeg.exe TwitchDownloaderWPF/bin/Release/net8.0-windows/publish/win-x64/ffmpeg.exe - name: Zip Windows GUI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderWPF/bin/Release/net6.0-windows/publish/win-x64" + files: "TwitchDownloaderWPF/bin/Release/net8.0-windows/publish/win-x64" dest: TwitchDownloaderGUI-Windows-x64.zip - name: Upload Windows GUI Artifact Asset @@ -57,7 +57,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore Dependencies run: dotnet restore TwitchDownloaderCLI - name: Build Windows CLI @@ -74,31 +74,31 @@ jobs: - name: Zip Windows CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/Windows" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/Windows" dest: TwitchDownloaderCLI-Windows-x64.zip - name: Zip Linux CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/Linux" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/Linux" dest: TwitchDownloaderCLI-Linux-x64.zip - name: Zip LinuxAlpine CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/LinuxAlpine" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/LinuxAlpine" dest: TwitchDownloaderCLI-LinuxAlpine-x64.zip - name: Zip LinuxArm CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/LinuxArm" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/LinuxArm" dest: TwitchDownloaderCLI-LinuxArm.zip - name: Zip LinuxArm64 CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/LinuxArm64" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/LinuxArm64" dest: TwitchDownloaderCLI-LinuxArm64.zip - name: Upload Windows CLI Artifact Asset @@ -139,7 +139,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore Dependencies run: dotnet restore TwitchDownloaderCLI - name: Build MacOS CLI @@ -150,13 +150,13 @@ jobs: - name: Zip MacOS CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/MacOS" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/MacOS" dest: TwitchDownloaderCLI-MacOS-x64.zip - name: Zip MacOSArm64 CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/MacOSArm64" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/MacOSArm64" dest: TwitchDownloaderCLI-MacOSArm64.zip - name: Upload MacOS CLI Artifact Asset diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66d76aa0..dbd5ad6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore Dependencies run: dotnet restore TwitchDownloaderWPF - name: Build Windows GUI @@ -60,12 +60,12 @@ jobs: file-name: ffmpeg.zip - name: Bundle FFmpeg - run: tar xfz ffmpeg.zip --strip-components=1; copy bin/ffmpeg.exe TwitchDownloaderWPF/bin/Release/net6.0-windows/publish/win-x64/ffmpeg.exe + run: tar xfz ffmpeg.zip --strip-components=1; copy bin/ffmpeg.exe TwitchDownloaderWPF/bin/Release/net8.0-windows/publish/win-x64/ffmpeg.exe - name: Zip Windows GUI Release Asset uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderWPF/bin/Release/net6.0-windows/publish/win-x64" + files: "TwitchDownloaderWPF/bin/Release/net8.0-windows/publish/win-x64" dest: TwitchDownloaderGUI-${{ github.event.inputs.release_tag }}-Windows-x64.zip - name: Download URL @@ -98,7 +98,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore Dependencies run: dotnet restore TwitchDownloaderCLI - name: Build Windows CLI @@ -115,31 +115,31 @@ jobs: - name: Zip Windows CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/Windows" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/Windows" dest: TwitchDownloaderCLI-${{ github.event.inputs.release_tag }}-Windows-x64.zip - name: Zip Linux CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/Linux" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/Linux" dest: TwitchDownloaderCLI-${{ github.event.inputs.release_tag }}-Linux-x64.zip - name: Zip LinuxAlpine CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/LinuxAlpine" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/LinuxAlpine" dest: TwitchDownloaderCLI-${{ github.event.inputs.release_tag }}-LinuxAlpine-x64.zip - name: Zip LinuxArm CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/LinuxArm" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/LinuxArm" dest: TwitchDownloaderCLI-${{ github.event.inputs.release_tag }}-LinuxArm.zip - name: Zip LinuxArm64 CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/LinuxArm64" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/LinuxArm64" dest: TwitchDownloaderCLI-${{ github.event.inputs.release_tag }}-LinuxArm64.zip - name: Download URL @@ -211,7 +211,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore Dependencies run: dotnet restore TwitchDownloaderCLI - name: Build MacOS CLI @@ -222,13 +222,13 @@ jobs: - name: Zip MacOS CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/MacOS" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/MacOS" dest: TwitchDownloaderCLI-${{ github.event.inputs.release_tag }}-MacOS-x64.zip - name: Zip MacOSArm64 CLI uses: vimtor/action-zip@v1.1 with: - files: "TwitchDownloaderCLI/bin/Release/net6.0/publish/MacOSArm64" + files: "TwitchDownloaderCLI/bin/Release/net8.0/publish/MacOSArm64" dest: TwitchDownloaderCLI-${{ github.event.inputs.release_tag }}-MacOSArm64.zip - name: Download URL diff --git a/README.md b/README.md index 0bad89b5..56e3b0fe 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ You can find more example commands in the [CLI README](TwitchDownloaderCLI/READM ## Requirements -- [.NET 6.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) +- [.NET 8.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) - About 1GB of disk space ## Build Instructions diff --git a/README_es.md b/README_es.md index add2fef3..84311eda 100644 --- a/README_es.md +++ b/README_es.md @@ -161,7 +161,7 @@ chmod +x ffmpeg ## Requisitos -- [.NET 6.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) +- [.NET 8.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) ## Instrucciones de Compilación diff --git a/README_it.md b/README_it.md index 37e20fec..9dc90e2a 100644 --- a/README_it.md +++ b/README_it.md @@ -188,7 +188,7 @@ Puoi trovare altri esempi nel [CLI README](TwitchDownloaderCLI/README.md#example ## Requisiti -- [.NET 6.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) +- [.NET 8.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) - Circa 1GB di spazio su disco ## Istruzioni diff --git a/README_ja.md b/README_ja.md index c72cbe0c..7cfe8bb8 100644 --- a/README_ja.md +++ b/README_ja.md @@ -187,7 +187,7 @@ chmod +x ffmpeg ## 必要条件 -- [.NET 6.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) +- [.NET 8.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) - 約1GBのディスク空き容量 ## ビルド手順 diff --git a/README_pt-br.md b/README_pt-br.md index 7f366f7c..af607843 100644 --- a/README_pt-br.md +++ b/README_pt-br.md @@ -188,7 +188,7 @@ Você pode encontrar mais comandos no [CLI README](TwitchDownloaderCLI/README.md ## Requerimentos -- [.NET 6.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) +- [.NET 8.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) - Mais ou menos 1GB de espaço de disco. ## Instruções para construção diff --git a/README_tr.md b/README_tr.md index e3881922..4e431439 100644 --- a/README_tr.md +++ b/README_tr.md @@ -157,7 +157,7 @@ chmod +x ffmpeg ## Gereksinimler -- [.NET 6.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) +- [.NET 8.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) ## Derleme Talimatları diff --git a/README_zh-cn.md b/README_zh-cn.md index 4a90cf52..3793e4d4 100644 --- a/README_zh-cn.md +++ b/README_zh-cn.md @@ -188,7 +188,7 @@ chmod +x ffmpeg ## 要求 -- [.NET 6.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) +- [.NET 8.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) - 约 1GB 磁盘空间 ## 构建说明 diff --git a/TwitchDownloaderCLI.Tests/TwitchDownloaderCLI.Tests.csproj b/TwitchDownloaderCLI.Tests/TwitchDownloaderCLI.Tests.csproj index a0b77556..9de12c84 100644 --- a/TwitchDownloaderCLI.Tests/TwitchDownloaderCLI.Tests.csproj +++ b/TwitchDownloaderCLI.Tests/TwitchDownloaderCLI.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/TwitchDownloaderCLI/Modes/FfmpegHandler.cs b/TwitchDownloaderCLI/Modes/FfmpegHandler.cs index d2a192c6..9cdc2ef1 100644 --- a/TwitchDownloaderCLI/Modes/FfmpegHandler.cs +++ b/TwitchDownloaderCLI/Modes/FfmpegHandler.cs @@ -4,9 +4,9 @@ using System.Linq; using System.Runtime.InteropServices; using System.Threading; -using Mono.Unix; using TwitchDownloaderCLI.Modes.Arguments; using TwitchDownloaderCLI.Tools; +using TwitchDownloaderCore; using TwitchDownloaderCore.Interfaces; using Xabe.FFmpeg; using Xabe.FFmpeg.Downloader; @@ -49,12 +49,7 @@ private static void DownloadFfmpeg(ITaskProgress progress) try { - var ffmpegFileInfo = new UnixFileInfo("ffmpeg") - { - FileAccessPermissions = FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite | FileAccessPermissions.GroupRead | FileAccessPermissions.OtherRead | - FileAccessPermissions.UserExecute | FileAccessPermissions.GroupExecute | FileAccessPermissions.OtherExecute - }; - ffmpegFileInfo.Refresh(); + TwitchHelper.Set777UnixFilePermissions(new FileInfo(FfmpegExecutableName)); } catch { diff --git a/TwitchDownloaderCLI/Properties/PublishProfiles/Linux.pubxml b/TwitchDownloaderCLI/Properties/PublishProfiles/Linux.pubxml index 2fea392c..b3d93c18 100644 --- a/TwitchDownloaderCLI/Properties/PublishProfiles/Linux.pubxml +++ b/TwitchDownloaderCLI/Properties/PublishProfiles/Linux.pubxml @@ -7,12 +7,13 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem Release Any CPU - net6.0 - bin\Release\net6.0\publish\Linux + net8.0 + bin\Release\net8.0\publish\Linux linux-x64 true True True + partial true \ No newline at end of file diff --git a/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxAlpine.pubxml b/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxAlpine.pubxml index 1adbee69..36eb462c 100644 --- a/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxAlpine.pubxml +++ b/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxAlpine.pubxml @@ -6,14 +6,15 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Release x64 - bin\Release\net6.0\publish\LinuxAlpine + bin\Release\net8.0\publish\LinuxAlpine FileSystem - net6.0 + net8.0 linux-musl-x64 true True False True + partial true \ No newline at end of file diff --git a/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxArm.pubxml b/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxArm.pubxml index e424a191..37bd5e6d 100644 --- a/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxArm.pubxml +++ b/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxArm.pubxml @@ -6,13 +6,14 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Release Any CPU - bin\Release\net6.0\publish\LinuxArm + bin\Release\net8.0\publish\LinuxArm FileSystem - net6.0 + net8.0 linux-arm true True True + partial true \ No newline at end of file diff --git a/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxArm64.pubxml b/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxArm64.pubxml index ce762606..e951ced2 100644 --- a/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxArm64.pubxml +++ b/TwitchDownloaderCLI/Properties/PublishProfiles/LinuxArm64.pubxml @@ -7,12 +7,13 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem Release Any CPU - net6.0 - bin\Release\net6.0\publish\LinuxArm64 + net8.0 + bin\Release\net8.0\publish\LinuxArm64 linux-arm64 true True True + partial true diff --git a/TwitchDownloaderCLI/Properties/PublishProfiles/MacOS.pubxml b/TwitchDownloaderCLI/Properties/PublishProfiles/MacOS.pubxml index 789cc72a..68f614d9 100644 --- a/TwitchDownloaderCLI/Properties/PublishProfiles/MacOS.pubxml +++ b/TwitchDownloaderCLI/Properties/PublishProfiles/MacOS.pubxml @@ -6,13 +6,14 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Release Any CPU - bin\Release\net6.0\publish\MacOS + bin\Release\net8.0\publish\MacOS FileSystem - net6.0 + net8.0 osx-x64 true True True + partial true true diff --git a/TwitchDownloaderCLI/Properties/PublishProfiles/MacOSArm64.pubxml b/TwitchDownloaderCLI/Properties/PublishProfiles/MacOSArm64.pubxml index 84087d4b..67b73c50 100644 --- a/TwitchDownloaderCLI/Properties/PublishProfiles/MacOSArm64.pubxml +++ b/TwitchDownloaderCLI/Properties/PublishProfiles/MacOSArm64.pubxml @@ -6,13 +6,14 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Release Any CPU - bin\Release\net6.0\publish\MacOSArm64 + bin\Release\net8.0\publish\MacOSArm64 FileSystem - net6.0 + net8.0 osx-arm64 true True True + partial true true diff --git a/TwitchDownloaderCLI/Properties/PublishProfiles/Windows.pubxml b/TwitchDownloaderCLI/Properties/PublishProfiles/Windows.pubxml index 94566a3c..6cc91ee5 100644 --- a/TwitchDownloaderCLI/Properties/PublishProfiles/Windows.pubxml +++ b/TwitchDownloaderCLI/Properties/PublishProfiles/Windows.pubxml @@ -6,14 +6,15 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Release x64 - bin\Release\net6.0\publish\Windows + bin\Release\net8.0\publish\Windows FileSystem - net6.0 + net8.0 win-x64 true True False True + partial true \ No newline at end of file diff --git a/TwitchDownloaderCLI/Tools/CliTaskProgress.cs b/TwitchDownloaderCLI/Tools/CliTaskProgress.cs index 7b873c82..61f5e523 100644 --- a/TwitchDownloaderCLI/Tools/CliTaskProgress.cs +++ b/TwitchDownloaderCLI/Tools/CliTaskProgress.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using TwitchDownloaderCLI.Models; using TwitchDownloaderCore.Interfaces; @@ -46,13 +47,13 @@ public void SetStatus(string status) } } - public void SetTemplateStatus(string status, int initialPercent) + public void SetTemplateStatus([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string statusTemplate, int initialPercent) { if ((_logLevel & LogLevel.Status) == 0) return; lock (this) { - _status = status; + _status = statusTemplate; _statusIsTemplate = true; if (!_lastWriteHadNewLine) @@ -65,13 +66,13 @@ public void SetTemplateStatus(string status, int initialPercent) } } - public void SetTemplateStatus(string status, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2) + public void SetTemplateStatus([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string statusTemplate, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2) { if ((_logLevel & LogLevel.Status) == 0) return; lock (this) { - _status = status; + _status = statusTemplate; _statusIsTemplate = true; if (!_lastWriteHadNewLine) diff --git a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj index 19138245..04dbf704 100644 --- a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj +++ b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj @@ -7,14 +7,13 @@ Download and render Twitch VODs, clips, and chats MIT AnyCPU;x64 - net6.0 + net8.0 default - diff --git a/TwitchDownloaderCore.Tests/TwitchDownloaderCore.Tests.csproj b/TwitchDownloaderCore.Tests/TwitchDownloaderCore.Tests.csproj index d7c3d66e..35981faa 100644 --- a/TwitchDownloaderCore.Tests/TwitchDownloaderCore.Tests.csproj +++ b/TwitchDownloaderCore.Tests/TwitchDownloaderCore.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/TwitchDownloaderCore/Chat/ChatJson.cs b/TwitchDownloaderCore/Chat/ChatJson.cs index 137f67b4..98a7b186 100644 --- a/TwitchDownloaderCore/Chat/ChatJson.cs +++ b/TwitchDownloaderCore/Chat/ChatJson.cs @@ -131,31 +131,30 @@ private static async Task GetJsonDocumentAsync(Stream stream, stri 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]) + switch (rentedBuffer) { - case (0x1F, 0x8B, _, _): // https://docs.fileformat.com/compression/gz/#gz-file-header + 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 + 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(); + var jsonString = await sr.ReadToEndAsync(cancellationToken); return JsonDocument.Parse(jsonString.AsMemory(), deserializationOptions); } - case (0xFE, 0xFF, _, _): // UTF-16 BE - case (0xFF, 0xFE, _, _): // UTF-16 LE + 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(); + var jsonString = await sr.ReadToEndAsync(cancellationToken); return JsonDocument.Parse(jsonString.AsMemory(), deserializationOptions); } - case (0xEF, 0xBB, 0xBF, _): // UTF-8 - case ((byte)'{', _, _, _): // Starts with a '{', probably JSON + case [0xEF, 0xBB, 0xBF, ..]: // UTF-8 + case [(byte)'{', ..]: // Starts with a '{', probably JSON { return await JsonDocument.ParseAsync(stream, deserializationOptions, cancellationToken); } @@ -231,7 +230,7 @@ private static async Task UpgradeChatJson(ChatRoot chatRoot) { foreach (var comment in chatRoot.comments) { - var bitMatch = TwitchRegex.BitsRegex.Match(comment.message.body); + 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 ef94a768..4de698bf 100644 --- a/TwitchDownloaderCore/ChatDownloader.cs +++ b/TwitchDownloaderCore/ChatDownloader.cs @@ -230,7 +230,7 @@ private static List ConvertComments(CommentVideo video, ChatFormat form message.body = bodyStringBuilder.ToString(); - var bitMatch = TwitchRegex.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/ChatRenderer.cs b/TwitchDownloaderCore/ChatRenderer.cs index 62bb68f6..57e2ae63 100644 --- a/TwitchDownloaderCore/ChatRenderer.cs +++ b/TwitchDownloaderCore/ChatRenderer.cs @@ -3,6 +3,7 @@ using SkiaSharp.HarfBuzz; using System; using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -23,7 +24,7 @@ namespace TwitchDownloaderCore { - public sealed class ChatRenderer : IDisposable + public partial class ChatRenderer : IDisposable { public bool Disposed { get; private set; } = false; public ChatRoot chatRoot { get; private set; } = new ChatRoot(); @@ -31,13 +32,16 @@ public sealed class ChatRenderer : IDisposable private static readonly SKColor Purple = SKColor.Parse("#7B2CF2"); private static readonly SKColor[] DefaultUsernameColors = { SKColor.Parse("#FF0000"), SKColor.Parse("#0000FF"), SKColor.Parse("#00FF00"), SKColor.Parse("#B22222"), SKColor.Parse("#FF7F50"), SKColor.Parse("#9ACD32"), SKColor.Parse("#FF4500"), SKColor.Parse("#2E8B57"), SKColor.Parse("#DAA520"), SKColor.Parse("#D2691E"), SKColor.Parse("#5F9EA0"), SKColor.Parse("#1E90FF"), SKColor.Parse("#FF69B4"), SKColor.Parse("#8A2BE2"), SKColor.Parse("#00FF7F") }; - private static readonly Regex RtlRegex = new("[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]", RegexOptions.Compiled); - private static readonly Regex BlockArtRegex = new("[\u2500-\u257F\u2580-\u259F\u2800-\u28FF]", RegexOptions.Compiled); - private static readonly Regex EmojiRegex = new(@"(?:[#*0-9]\uFE0F?\u20E3|©\uFE0F?|[®\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26F9(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC3\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDD-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE])))?))?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDD75(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF6](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE74\uDE78-\uDE7C\uDE80-\uDE86\uDE90-\uDEAC\uDEB0-\uDEBA\uDEC0-\uDEC2\uDED0-\uDED9\uDEE0-\uDEE7]|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF])?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?))", - RegexOptions.Compiled); + [GeneratedRegex("[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]")] + private static partial Regex RtlRegex(); - // TODO: Use FrozenDictionary when .NET 8 - private static readonly IReadOnlyDictionary AllEmojiSequences = Emoji.All.ToDictionary(e => e.SortOrder, e => e.Sequence.AsString); + [GeneratedRegex("[\u2500-\u257F\u2580-\u259F\u2800-\u28FF]")] + private static partial Regex BlockArtRegex(); + + [GeneratedRegex(@"(?:[#*0-9]\uFE0F?\u20E3|©\uFE0F?|[®\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26F9(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC3\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDD-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE])))?))?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDD75(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF6](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE74\uDE78-\uDE7C\uDE80-\uDE86\uDE90-\uDEAC\uDEB0-\uDEBA\uDEC0-\uDEC2\uDED0-\uDED9\uDEE0-\uDEE7]|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF])?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?))")] + private static partial Regex EmojiRegex(); + + private static readonly IReadOnlyDictionary AllEmojiSequences = Emoji.All.ToFrozenDictionary(e => e.SortOrder, e => e.Sequence.AsString); private readonly ITaskProgress _progress; private readonly ChatRenderOptions renderOptions; @@ -872,7 +876,7 @@ private void DrawFragmentPart(List<(SKImageInfo info, SKBitmap bitmap)> sectionI { DrawThirdPartyEmote(sectionImages, emotePositionList, ref drawPos, defaultPos, emote, highlightWords); } - else if (!skipEmoji && EmojiRegex.IsMatch(fragmentPart)) + else if (!skipEmoji && EmojiRegex().IsMatch(fragmentPart)) { DrawEmojiMessage(sectionImages, emotePositionList, ref drawPos, defaultPos, bitsCount, fragmentPart, highlightWords); } @@ -1048,10 +1052,9 @@ private void DrawEmojiMessage(List<(SKImageInfo info, SKBitmap bitmap)> sectionI private void DrawNonFontMessage(List<(SKImageInfo info, SKBitmap bitmap)> sectionImages, ref Point drawPos, Point defaultPos, string fragmentString, bool highlightWords) { - ReadOnlySpan fragmentSpan = fragmentString.AsSpan().Trim('\uFE0F'); + var fragmentSpan = fragmentString.AsSpan().Trim('\uFE0F'); - // TODO: use fragmentSpan instead of fragmentString once upgraded to .NET 7 - if (BlockArtRegex.IsMatch(fragmentString)) + if (BlockArtRegex().IsMatch(fragmentSpan)) { // Very rough estimation of width of block art int textWidth = (int)(fragmentSpan.Length * renderOptions.BlockArtCharWidth); @@ -1298,7 +1301,7 @@ private void DrawText(string drawText, SKPaint textFont, bool padding, List<(SKI sectionImageCanvas.DrawPath(outlinePath, outlinePaint); } - if (RtlRegex.IsMatch(drawText)) + if (RtlRegex().IsMatch(drawText)) { sectionImageCanvas.DrawShapedText(drawText, drawPos.X, drawPos.Y, textFont); } diff --git a/TwitchDownloaderCore/Extensions/M3U8Extensions.cs b/TwitchDownloaderCore/Extensions/M3U8Extensions.cs index 3ef590fb..7e840fae 100644 --- a/TwitchDownloaderCore/Extensions/M3U8Extensions.cs +++ b/TwitchDownloaderCore/Extensions/M3U8Extensions.cs @@ -5,7 +5,7 @@ namespace TwitchDownloaderCore.Extensions { - public static class M3U8Extensions + public static partial class M3U8Extensions { public static void SortStreamsByQuality(this M3U8 m3u8) { @@ -21,8 +21,8 @@ public static void SortStreamsByQuality(this M3U8 m3u8) } } - private static readonly Regex UserQualityStringRegex = new(@"(?:^|\s)(?:(?\d{3,4})x)?(?\d{3,4})p?(?\d{1,3})?(?:$|\s)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); + [GeneratedRegex(@"(?:^|\s)(?:(?\d{3,4})x)?(?\d{3,4})p?(?\d{1,3})?(?:$|\s)", RegexOptions.IgnoreCase)] + private static partial Regex UserQualityStringRegex(); public static M3U8.Stream GetStreamOfQuality(this M3U8 m3u8, string qualityString) { @@ -48,7 +48,7 @@ public static M3U8.Stream GetStreamOfQuality(this M3U8 m3u8, string qualityStrin } } - var qualityStringMatch = UserQualityStringRegex.Match(qualityString); + var qualityStringMatch = UserQualityStringRegex().Match(qualityString); if (!qualityStringMatch.Success) { return m3u8.BestQualityStream(); @@ -105,27 +105,28 @@ private static bool TryGetKeywordStream(M3U8 m3u8, string qualityString, out M3U return false; } + [GeneratedRegex(@"\d{3,4}p\d{2,3}")] + private static partial Regex ResolutionFramerateRegex(); + /// /// A representing the 's /// and in the format of "{resolution}p{framerate}" or /// public static string GetResolutionFramerateString(this M3U8.Stream stream) { - const string RESOLUTION_FRAMERATE_PATTERN = /*lang=regex*/@"\d{3,4}p\d{2,3}"; - var mediaInfo = stream.MediaInfo; - if (stream.IsAudioOnly() || Regex.IsMatch(mediaInfo.Name, RESOLUTION_FRAMERATE_PATTERN)) + if (stream.IsAudioOnly() || ResolutionFramerateRegex().IsMatch(mediaInfo.Name)) { return mediaInfo.Name; } var streamInfo = stream.StreamInfo; - if (Regex.IsMatch(streamInfo.Video, RESOLUTION_FRAMERATE_PATTERN)) + if (ResolutionFramerateRegex().IsMatch(streamInfo.Video)) { return streamInfo.Video; } - if (Regex.IsMatch(mediaInfo.GroupId, RESOLUTION_FRAMERATE_PATTERN)) + if (ResolutionFramerateRegex().IsMatch(mediaInfo.GroupId)) { return mediaInfo.GroupId; } diff --git a/TwitchDownloaderCore/Interfaces/ITaskProgress.cs b/TwitchDownloaderCore/Interfaces/ITaskProgress.cs index a14460e4..09638440 100644 --- a/TwitchDownloaderCore/Interfaces/ITaskProgress.cs +++ b/TwitchDownloaderCore/Interfaces/ITaskProgress.cs @@ -1,13 +1,13 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace TwitchDownloaderCore.Interfaces { - // TODO: Add StringSyntaxAttributes when .NET 7+ public interface ITaskProgress : ITaskLogger { void SetStatus(string status); - void SetTemplateStatus(string status, int initialPercent); - void SetTemplateStatus(string status, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2); + void SetTemplateStatus([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string statusTemplate, int initialPercent); + void SetTemplateStatus([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string statusTemplate, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2); void ReportProgress(int percent); void ReportProgress(int percent, TimeSpan time1, TimeSpan time2); } diff --git a/TwitchDownloaderCore/Tools/FilenameService.cs b/TwitchDownloaderCore/Tools/FilenameService.cs index 8f73faeb..d8f95b66 100644 --- a/TwitchDownloaderCore/Tools/FilenameService.cs +++ b/TwitchDownloaderCore/Tools/FilenameService.cs @@ -7,8 +7,20 @@ namespace TwitchDownloaderCore.Tools { - public static class FilenameService + public static partial class FilenameService { + [GeneratedRegex("{date_custom=\"(.*?)\"}")] + private static partial Regex DateCustomRegex(); + + [GeneratedRegex("{trim_start_custom=\"(.*?)\"}")] + private static partial Regex TrimStartCustomRegex(); + + [GeneratedRegex("{trim_end_custom=\"(.*?)\"}")] + private static partial Regex TrimEndCustomRegex(); + + [GeneratedRegex("{length_custom=\"(.*?)\"}")] + private static partial Regex LengthCustomRegex(); + public static string GetFilename(string template, string title, string id, DateTime date, string channel, TimeSpan trimStart, TimeSpan trimEnd, long viewCount, string game) { var videoLength = trimEnd - trimStart; @@ -27,26 +39,22 @@ public static string GetFilename(string template, string title, string id, DateT if (template.Contains("{date_custom=")) { - var dateRegex = new Regex("{date_custom=\"(.*?)\"}"); - ReplaceCustomWithFormattable(stringBuilder, dateRegex, date); + ReplaceCustomWithFormattable(stringBuilder, DateCustomRegex(), date); } if (template.Contains("{trim_start_custom=")) { - var trimStartRegex = new Regex("{trim_start_custom=\"(.*?)\"}"); - ReplaceCustomWithFormattable(stringBuilder, trimStartRegex, trimStart); + ReplaceCustomWithFormattable(stringBuilder, TrimStartCustomRegex(), trimStart); } if (template.Contains("{trim_end_custom=")) { - var trimEndRegex = new Regex("{trim_end_custom=\"(.*?)\"}"); - ReplaceCustomWithFormattable(stringBuilder, trimEndRegex, trimEnd); + ReplaceCustomWithFormattable(stringBuilder, TrimEndCustomRegex(), trimEnd); } if (template.Contains("{length_custom=")) { - var lengthRegex = new Regex("{length_custom=\"(.*?)\"}"); - ReplaceCustomWithFormattable(stringBuilder, lengthRegex, videoLength); + ReplaceCustomWithFormattable(stringBuilder, LengthCustomRegex(), videoLength); } var fileName = stringBuilder.ToString(); @@ -59,7 +67,6 @@ private static void ReplaceCustomWithFormattable(StringBuilder sb, Regex regex, do { // There's probably a better way to do this that doesn't require calling ToString() - // However we need .NET7+ for span support in the regex matcher. var match = regex.Match(sb.ToString()); if (!match.Success) break; diff --git a/TwitchDownloaderCore/Tools/HighlightIcons.cs b/TwitchDownloaderCore/Tools/HighlightIcons.cs index aee70c39..c5eb996b 100644 --- a/TwitchDownloaderCore/Tools/HighlightIcons.cs +++ b/TwitchDownloaderCore/Tools/HighlightIcons.cs @@ -27,7 +27,7 @@ public enum HighlightType Unknown } - public sealed class HighlightIcons : IDisposable + public sealed partial class HighlightIcons : IDisposable { public bool Disposed { get; private set; } @@ -43,9 +43,14 @@ public sealed class HighlightIcons : IDisposable private const int ICON_SIZE = 72; // Icon SVG strings are scaled for 72x72 - private static readonly Regex SubMessageRegex = new(@"^((?:\w+ )?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 static readonly Regex WatchStreakRegex = new(@"^((?:\w+ )?watched \d+ consecutive streams this month and sparked a watch streak! )(.+)$", RegexOptions.Compiled); + [GeneratedRegex(@"^((?:\w+ )?subscribed (?:with Prime|at Tier \d)\. They've subscribed for \d{1,3} months(?:, currently on a \d{1,3} month streak)?! )(.+)$")] + private static partial Regex SubMessageRegex(); + + [GeneratedRegex(@"^An anonymous user (?:gifted a|is gifting \d{1,4}) Tier \d")] + private static partial Regex GiftAnonymousRegex(); + + [GeneratedRegex(@"^((?:\w+ )?watched \d+ consecutive streams this month and sparked a watch streak! )(.+)$")] + private static partial Regex WatchStreakRegex(); private SKImage _subscribedTierIcon; private SKImage _subscribedPrimeIcon; @@ -118,20 +123,20 @@ public static HighlightType GetHighlightType(Comment comment) if (bodyWithoutName.StartsWith(" converted from a")) { - // TODO: use bodyWithoutName when .NET 7 - var convertedToMatch = Regex.Match(comment.message.body, $@"(?<=^{comment.commenter.display_name} converted from a (?:Prime|Tier \d) sub to a )(?:Prime|Tier \d)"); - if (!convertedToMatch.Success) - return HighlightType.None; - - // TODO: use ValueSpan when .NET 7 - return convertedToMatch.Value switch + var slice = bodyWithoutName[17..]; + foreach (var match in Regex.EnumerateMatches(slice, @"(?<= (?:Prime|Tier \d) sub to a )(?:Prime|Tier \d)")) { - "Prime" => HighlightType.SubscribedPrime, - "Tier 1" => HighlightType.SubscribedTier, - "Tier 2" => HighlightType.SubscribedTier, - "Tier 3" => HighlightType.SubscribedTier, - _ => HighlightType.None - }; + return slice.Slice(match.Index, match.Length) switch + { + "Prime" => HighlightType.SubscribedPrime, + "Tier 1" => HighlightType.SubscribedTier, + "Tier 2" => HighlightType.SubscribedTier, + "Tier 3" => HighlightType.SubscribedTier, + _ => HighlightType.None + }; + } + + return HighlightType.None; } } @@ -140,13 +145,12 @@ public static HighlightType GetHighlightType(Comment comment) if (char.IsDigit(bodySpan[0]) && bodySpan.EndsWith(" have joined! ")) { - // TODO: use bodySpan when .NET 7 - if (Regex.IsMatch(comment.message.body, $@"^\d+ raiders from {comment.commenter.display_name} have joined! ")) + if (Regex.IsMatch(bodySpan, $@"^\d+ raiders from {comment.commenter.display_name} have joined! ")) 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)) + if (comment.commenter._id is ANONYMOUS_GIFT_ACCOUNT_ID && GiftAnonymousRegex().IsMatch(comment.message.body)) return HighlightType.GiftedAnonymous; return HighlightType.None; @@ -243,7 +247,7 @@ private SKPaint GetSvgIconPaint(SKColor iconColor) /// public static (Comment subMessage, Comment customMessage) SplitSubComment(Comment comment) { - var subMessageMatch = SubMessageRegex.Match(comment.message.body); + var subMessageMatch = SubMessageRegex().Match(comment.message.body); if (!subMessageMatch.Success) { // Return the original comment + null if there is no custom sub message @@ -291,7 +295,7 @@ public static (Comment subMessage, Comment customMessage) SplitSubComment(Commen /// public static (Comment subMessage, Comment customMessage) SplitWatchStreakComment(Comment comment) { - var watchStreakMatch = WatchStreakRegex.Match(comment.message.body); + var watchStreakMatch = WatchStreakRegex().Match(comment.message.body); if (!watchStreakMatch.Success) { // Return the original comment + null if there is no custom watch streak message diff --git a/TwitchDownloaderCore/Tools/StubTaskProgress.cs b/TwitchDownloaderCore/Tools/StubTaskProgress.cs index a044430e..fc25a7e0 100644 --- a/TwitchDownloaderCore/Tools/StubTaskProgress.cs +++ b/TwitchDownloaderCore/Tools/StubTaskProgress.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using TwitchDownloaderCore.Interfaces; @@ -30,9 +31,9 @@ public void LogFfmpeg(string logMessage) { } public void SetStatus(string status) { } - public void SetTemplateStatus(string status, int initialPercent) { } + public void SetTemplateStatus([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string statusTemplate, int initialPercent) { } - public void SetTemplateStatus(string status, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2) { } + public void SetTemplateStatus([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string statusTemplate, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2) { } public void ReportProgress(int percent) { } diff --git a/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs b/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs index 3e711b81..0babd791 100644 --- a/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs +++ b/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Text; using TwitchDownloaderCore.Extensions; @@ -20,7 +21,7 @@ public object GetFormat(Type formatType) return null; } - public string Format(string format, object arg, IFormatProvider formatProvider = null) + public string Format([StringSyntax(StringSyntaxAttribute.TimeSpanFormat)] string format, object arg, IFormatProvider formatProvider = null) { if (arg is TimeSpan timeSpan) { @@ -32,7 +33,7 @@ public string Format(string format, object arg, IFormatProvider formatProvider = /// Provides an identical output to but without boxing the . /// This method is not part of the interface. - public string Format(string format, TimeSpan timeSpan, IFormatProvider formatProvider = null) + public string Format([StringSyntax(StringSyntaxAttribute.TimeSpanFormat)] string format, TimeSpan timeSpan, IFormatProvider formatProvider = null) { if (string.IsNullOrEmpty(format)) { diff --git a/TwitchDownloaderCore/Tools/TwitchRegex.cs b/TwitchDownloaderCore/Tools/TwitchRegex.cs index fa5f718c..98a198aa 100644 --- a/TwitchDownloaderCore/Tools/TwitchRegex.cs +++ b/TwitchDownloaderCore/Tools/TwitchRegex.cs @@ -3,28 +3,33 @@ namespace TwitchDownloaderCore.Tools { - public static class TwitchRegex + public static partial 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+\/v(?:ideo)?\/)\d+(?=$|\?|\s)", RegexOptions.Compiled); - private static readonly Regex ClipId = new(@"(?<=^|(?:clips\.)?twitch\.tv\/(?:\w+\/clip\/)?)[\w-]+?(?=$|\?|\s)", RegexOptions.Compiled); + [GeneratedRegex("@\"(?<=^|twitch\\.tv\\/videos\\/)\\d+(?=$|\\?|\\s)\"")] + private static partial Regex VideoId(); - 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{0,6}(?=\s|$)", - RegexOptions.Compiled); + [GeneratedRegex(@"(?<=^|twitch\.tv\/\w+\/v(?:ideo)?\/)\d+(?=$|\?|\s)")] + private static partial Regex HighlightId(); + + [GeneratedRegex(@"(?<=^|(?:clips\.)?twitch\.tv\/(?:\w+\/clip\/)?)[\w-]+?(?=$|\?|\s)")] + private static partial Regex ClipId(); + + [GeneratedRegex(@"(?<=(?:\?|&)t=)\d+h\d+m\d+s(?=$|\?|\s)")] + public static partial Regex UrlTimeCode(); + + [GeneratedRegex(@"(?<=(?:\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{0,6}(?=\s|$)")] + public static partial Regex BitsRegex(); /// A of the video's id or . public static Match MatchVideoId(string text) { - var videoIdMatch = VideoId.Match(text); + var videoIdMatch = VideoId().Match(text); if (videoIdMatch.Success) { return videoIdMatch; } - var highlightIdMatch = HighlightId.Match(text); + var highlightIdMatch = HighlightId().Match(text); if (highlightIdMatch.Success) { return highlightIdMatch; @@ -36,7 +41,7 @@ public static Match MatchVideoId(string text) /// A of the clip's id or . public static Match MatchClipId(string text) { - var clipIdMatch = ClipId.Match(text); + var clipIdMatch = ClipId().Match(text); if (clipIdMatch.Success && !clipIdMatch.Value.All(char.IsDigit)) { return clipIdMatch; diff --git a/TwitchDownloaderCore/TwitchDownloaderCore.csproj b/TwitchDownloaderCore/TwitchDownloaderCore.csproj index 267ca24f..6361a19f 100644 --- a/TwitchDownloaderCore/TwitchDownloaderCore.csproj +++ b/TwitchDownloaderCore/TwitchDownloaderCore.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 https://github.com/lay295/TwitchDownloader true MIT @@ -25,7 +25,6 @@ - diff --git a/TwitchDownloaderCore/TwitchHelper.cs b/TwitchDownloaderCore/TwitchHelper.cs index 06b1d5e4..5410160c 100644 --- a/TwitchDownloaderCore/TwitchHelper.cs +++ b/TwitchDownloaderCore/TwitchHelper.cs @@ -9,6 +9,7 @@ using System.Net.Http; using System.Net.Http.Json; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; @@ -22,7 +23,7 @@ namespace TwitchDownloaderCore { - public static class TwitchHelper + public static partial class TwitchHelper { private static readonly HttpClient httpClient = new HttpClient(); private static readonly string[] BttvZeroWidth = { "SoSnowy", "IceCold", "SantaHat", "TopHat", "ReinDeer", "CandyCane", "cvMask", "cvHazmat" }; @@ -671,6 +672,9 @@ public static async Task> GetChatBadges(List comments, return returnList; } + [GeneratedRegex(@"\.(?:png|PNG)$", RegexOptions.RightToLeft)] + private static partial Regex EmojiExtensionRegex(); + public static async Task> GetEmojis(string cacheFolder, EmojiVendor emojiVendor, ITaskLogger logger, CancellationToken cancellationToken = default) { var returnCache = new Dictionary(); @@ -679,13 +683,12 @@ public static async Task> GetEmojis(string cacheFol return returnCache; var emojiFolder = Path.Combine(cacheFolder, "emojis", emojiVendor.EmojiFolder()); - var emojiExtensions = new Regex(@"\.(?:png|PNG)$", RegexOptions.RightToLeft); // Extensions are case sensitive on Linux and Mac if (!Directory.Exists(emojiFolder)) CreateDirectory(emojiFolder); var emojiFiles = Directory.GetFiles(emojiFolder) - .Where(i => emojiExtensions.IsMatch(i)).ToArray(); + .Where(i => EmojiExtensionRegex().IsMatch(i)).ToArray(); if (emojiFiles.Length < emojiVendor.EmojiCount()) { @@ -719,7 +722,7 @@ public static async Task> GetEmojis(string cacheFol } emojiFiles = Directory.GetFiles(emojiFolder) - .Where(i => emojiExtensions.IsMatch(i)).ToArray(); + .Where(i => EmojiExtensionRegex().IsMatch(i)).ToArray(); } finally { @@ -886,7 +889,7 @@ public static FileInfo ClaimFile(string path, Func fileAlrea return fileInfo; } - public static DirectoryInfo CreateDirectory(string path) + public static DirectoryInfo CreateDirectory(string path, ITaskLogger logger = null) { DirectoryInfo directoryInfo = Directory.CreateDirectory(path); @@ -894,19 +897,26 @@ public static DirectoryInfo CreateDirectory(string path) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - SetDirectoryPermissions(path); + Set777UnixFilePermissions(directoryInfo); } } - catch { } + catch (Exception e) + { + logger?.LogVerbose($"Failed to set unix file mode for {directoryInfo.FullName}: {e.Message}"); + } return directoryInfo; } - public static void SetDirectoryPermissions(string path) + [UnsupportedOSPlatform("windows")] + public static FileSystemInfo Set777UnixFilePermissions(FileSystemInfo fileSystemInfo) { - var folderInfo = new Mono.Unix.UnixFileInfo(path); - folderInfo.FileAccessPermissions = Mono.Unix.FileAccessPermissions.AllPermissions; - folderInfo.Refresh(); + fileSystemInfo.UnixFileMode = UnixFileMode.OtherExecute | UnixFileMode.OtherWrite | UnixFileMode.OtherRead + | UnixFileMode.GroupExecute | UnixFileMode.GroupWrite | UnixFileMode.GroupRead + | UnixFileMode.UserExecute | UnixFileMode.UserWrite | UnixFileMode.UserRead; + + fileSystemInfo.Refresh(); + return fileSystemInfo; } /// diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs index 5a109a3a..65d9cf08 100644 --- a/TwitchDownloaderCore/VideoDownloader.cs +++ b/TwitchDownloaderCore/VideoDownloader.cs @@ -19,7 +19,7 @@ namespace TwitchDownloaderCore { - public sealed class VideoDownloader + public sealed partial class VideoDownloader { private readonly VideoDownloadOptions downloadOptions; private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(30) }; @@ -348,7 +348,6 @@ private int RunFfmpegVideoCopy(string tempFolder, FileInfo outputFile, string me } }; - var encodingTimeRegex = new Regex(@"(?<=time=)(\d\d):(\d\d):(\d\d)\.(\d\d)", RegexOptions.Compiled); var logQueue = new ConcurrentQueue(); process.ErrorDataReceived += (sender, e) => @@ -358,7 +357,7 @@ private int RunFfmpegVideoCopy(string tempFolder, FileInfo outputFile, string me logQueue.Enqueue(e.Data); // We cannot use -report ffmpeg arg because it redirects stderr - HandleFfmpegOutput(e.Data, encodingTimeRegex, seekDuration); + HandleFfmpegOutput(e.Data, seekDuration); }; process.Start(); @@ -377,9 +376,12 @@ private int RunFfmpegVideoCopy(string tempFolder, FileInfo outputFile, string me return process.ExitCode; } - private void HandleFfmpegOutput(string output, Regex encodingTimeRegex, TimeSpan videoLength) + [GeneratedRegex(@"(?<=time=)(\d\d):(\d\d):(\d\d)\.(\d\d)")] + private static partial Regex EncodingTimeRegex(); + + private void HandleFfmpegOutput(string output, TimeSpan videoLength) { - var encodingTimeMatch = encodingTimeRegex.Match(output); + var encodingTimeMatch = EncodingTimeRegex().Match(output); if (!encodingTimeMatch.Success) return; diff --git a/TwitchDownloaderWPF/PageChatDownload.xaml.cs b/TwitchDownloaderWPF/PageChatDownload.xaml.cs index 4cf35224..4ebc27b6 100644 --- a/TwitchDownloaderWPF/PageChatDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageChatDownload.xaml.cs @@ -134,7 +134,7 @@ private async Task GetVideoInfo() streamerId = int.Parse(videoInfo.data.video.owner.id); viewCount = videoInfo.data.video.viewCount; game = videoInfo.data.video.game?.displayName ?? Translations.Strings.UnknownGame; - var urlTimeCodeMatch = TwitchRegex.UrlTimeCode.Match(textUrl.Text); + var urlTimeCodeMatch = TwitchRegex.UrlTimeCode().Match(textUrl.Text); if (urlTimeCodeMatch.Success) { var time = UrlTimeCode.Parse(urlTimeCodeMatch.ValueSpan); diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs index 802fdf53..59116f1f 100644 --- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs @@ -140,7 +140,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 = TwitchRegex.UrlTimeCode.Match(textUrl.Text); + var urlTimeCodeMatch = TwitchRegex.UrlTimeCode().Match(textUrl.Text); if (urlTimeCodeMatch.Success) { var time = UrlTimeCode.Parse(urlTimeCodeMatch.ValueSpan); diff --git a/TwitchDownloaderWPF/Properties/PublishProfiles/Windows.pubxml b/TwitchDownloaderWPF/Properties/PublishProfiles/Windows.pubxml index 1f8a26b3..aeeb5abf 100644 --- a/TwitchDownloaderWPF/Properties/PublishProfiles/Windows.pubxml +++ b/TwitchDownloaderWPF/Properties/PublishProfiles/Windows.pubxml @@ -6,9 +6,9 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Release x64 - bin\Release\net6.0-windows\publish\win-x64\ + bin\Release\net8.0-windows\publish\win-x64\ FileSystem - net6.0-windows + net8.0-windows win-x64 true True diff --git a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj index a7f3f880..6dbf7cd7 100644 --- a/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj +++ b/TwitchDownloaderWPF/TwitchDownloaderWPF.csproj @@ -1,6 +1,6 @@  - net6.0-windows + net8.0-windows WinExe false publish\ diff --git a/TwitchDownloaderWPF/Utils/WpfTaskProgress.cs b/TwitchDownloaderWPF/Utils/WpfTaskProgress.cs index 02a8fae3..355f3ed5 100644 --- a/TwitchDownloaderWPF/Utils/WpfTaskProgress.cs +++ b/TwitchDownloaderWPF/Utils/WpfTaskProgress.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using TwitchDownloaderCore.Interfaces; using TwitchDownloaderWPF.Models; @@ -57,11 +58,11 @@ public void SetStatus(string status) } } - public void SetTemplateStatus(string status, int initialPercent) + public void SetTemplateStatus([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string statusTemplate, int initialPercent) { lock (this) { - _status = status; + _status = statusTemplate; _statusIsTemplate = true; _lastPercent = -1; // Ensure that the progress report runs @@ -69,11 +70,11 @@ public void SetTemplateStatus(string status, int initialPercent) } } - public void SetTemplateStatus(string status, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2) + public void SetTemplateStatus([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string statusTemplate, int initialPercent, TimeSpan initialTime1, TimeSpan initialTime2) { lock (this) { - _status = status; + _status = statusTemplate; _statusIsTemplate = true; _lastPercent = -1; // Ensure that the progress report runs diff --git a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs index b38fd225..14407766 100644 --- a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs +++ b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs @@ -5,6 +5,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Media; +using Microsoft.Win32; using TwitchDownloaderCore.Options; using TwitchDownloaderCore.Tools; using TwitchDownloaderWPF.Properties; @@ -625,12 +626,15 @@ private void EnqueueDataList() private void btnFolder_Click(object sender, RoutedEventArgs e) { - var dialog = new Ookii.Dialogs.Wpf.VistaFolderBrowserDialog(); + var dialog = new OpenFolderDialog(); if (Directory.Exists(textFolder.Text)) - dialog.RootFolder = dialog.RootFolder; + { + dialog.InitialDirectory = textFolder.Text; + } + if (dialog.ShowDialog(this).GetValueOrDefault()) { - textFolder.Text = dialog.SelectedPath; + textFolder.Text = dialog.FolderName; Settings.Default.QueueFolder = textFolder.Text; Settings.Default.Save(); } diff --git a/TwitchDownloaderWPF/WindowSettings.xaml.cs b/TwitchDownloaderWPF/WindowSettings.xaml.cs index 6cfc8ecf..887f4eef 100644 --- a/TwitchDownloaderWPF/WindowSettings.xaml.cs +++ b/TwitchDownloaderWPF/WindowSettings.xaml.cs @@ -5,6 +5,7 @@ using System.Windows; using System.Windows.Controls; using HandyControl.Data; +using Microsoft.Win32; using TwitchDownloaderWPF.Models; using TwitchDownloaderWPF.Properties; using TwitchDownloaderWPF.Services; @@ -28,10 +29,15 @@ public WindowSettings() private void BtnTempBrowse_Click(object sender, RoutedEventArgs e) { - var dialog = new Ookii.Dialogs.Wpf.VistaFolderBrowserDialog(); + var dialog = new OpenFolderDialog(); + if (Directory.Exists(TextTempPath.Text)) + { + dialog.InitialDirectory = TextTempPath.Text; + } + if (dialog.ShowDialog(this).GetValueOrDefault()) { - TextTempPath.Text = dialog.SelectedPath; + TextTempPath.Text = dialog.FolderName; } }