diff --git a/TwitchDownloaderCLI/Modes/DownloadChat.cs b/TwitchDownloaderCLI/Modes/DownloadChat.cs
index 152cc780..07a8ba45 100644
--- a/TwitchDownloaderCLI/Modes/DownloadChat.cs
+++ b/TwitchDownloaderCLI/Modes/DownloadChat.cs
@@ -31,7 +31,7 @@ private static ChatDownloadOptions GetDownloadOptions(ChatDownloadArgs inputOpti
Environment.Exit(1);
}
- var vodClipIdMatch = TwitchRegex.MatchVideoOrClipId(inputOptions.Id);
+ var vodClipIdMatch = IdParse.MatchVideoOrClipId(inputOptions.Id);
if (vodClipIdMatch is not { Success: true })
{
logger.LogError("Unable to parse Vod/Clip ID/URL.");
diff --git a/TwitchDownloaderCLI/Modes/DownloadClip.cs b/TwitchDownloaderCLI/Modes/DownloadClip.cs
index 24c44c26..a3e137a4 100644
--- a/TwitchDownloaderCLI/Modes/DownloadClip.cs
+++ b/TwitchDownloaderCLI/Modes/DownloadClip.cs
@@ -36,7 +36,7 @@ private static ClipDownloadOptions GetDownloadOptions(ClipDownloadArgs inputOpti
Environment.Exit(1);
}
- var clipIdMatch = TwitchRegex.MatchClipId(inputOptions.Id);
+ var clipIdMatch = IdParse.MatchClipId(inputOptions.Id);
if (clipIdMatch is not { Success: true })
{
logger.LogError("Unable to parse Clip ID/URL.");
diff --git a/TwitchDownloaderCLI/Modes/DownloadVideo.cs b/TwitchDownloaderCLI/Modes/DownloadVideo.cs
index 4ab857b8..fe0043df 100644
--- a/TwitchDownloaderCLI/Modes/DownloadVideo.cs
+++ b/TwitchDownloaderCLI/Modes/DownloadVideo.cs
@@ -34,7 +34,7 @@ private static VideoDownloadOptions GetDownloadOptions(VideoDownloadArgs inputOp
Environment.Exit(1);
}
- var vodIdMatch = TwitchRegex.MatchVideoId(inputOptions.Id);
+ var vodIdMatch = IdParse.MatchVideoId(inputOptions.Id);
if (vodIdMatch is not { Success: true })
{
logger.LogError("Unable to parse Vod ID/URL.");
diff --git a/TwitchDownloaderCore.Tests/ToolTests/IdParseTests.cs b/TwitchDownloaderCore.Tests/ToolTests/IdParseTests.cs
new file mode 100644
index 00000000..713ff682
--- /dev/null
+++ b/TwitchDownloaderCore.Tests/ToolTests/IdParseTests.cs
@@ -0,0 +1,134 @@
+using TwitchDownloaderCore.Tools;
+
+namespace TwitchDownloaderCore.Tests.ToolTests
+{
+ // ReSharper disable StringLiteralTypo
+ public class IdParseTests
+ {
+ [Theory]
+ [InlineData("41546181")] // Oldest VODs - 8
+ [InlineData("982306410")] // Old VODs - 9
+ [InlineData("6834869128")] // Current VODs - 10
+ [InlineData("11987163407")] // Future VODs - 11
+ public void CorrectlyParsesVodId(string id)
+ {
+ var match = IdParse.MatchVideoId(id);
+
+ Assert.NotNull(match);
+ Assert.Equal(id, match.Value);
+ }
+
+ [Theory]
+ [InlineData("https://www.twitch.tv/videos/41546181", "41546181")] // Oldest VODs - 8
+ [InlineData("https://www.twitch.tv/videos/982306410", "982306410")] // Old VODs - 9
+ [InlineData("https://www.twitch.tv/videos/6834869128", "6834869128")] // Current VODs - 10
+ [InlineData("https://www.twitch.tv/videos/11987163407", "11987163407")] // Future VODs - 11
+ [InlineData("https://www.twitch.tv/kitboga/video/2865132173", "2865132173")] // Alternate highlight URL
+ [InlineData("https://www.twitch.tv/kitboga/v/2865132173", "2865132173")] // Alternate highlight URL
+ [InlineData("https://www.twitch.tv/videos/4894164023/", "4894164023")]
+ public void CorrectlyParsesVodLink(string link, string expectedId)
+ {
+ var match = IdParse.MatchVideoId(link);
+
+ Assert.NotNull(match);
+ Assert.Equal(expectedId, match.Value);
+ }
+
+ [Theory]
+ [InlineData("SpineyPieTwitchRPGNurturing")]
+ [InlineData("FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf")]
+ public void CorrectlyParsesClipId(string id)
+ {
+ var match = IdParse.MatchClipId(id);
+
+ Assert.NotNull(match);
+ Assert.Equal(id, match.Value);
+ }
+
+ [Theory]
+ [InlineData("https://www.twitch.tv/streamer8/clip/SpineyPieTwitchRPGNurturing", "SpineyPieTwitchRPGNurturing")]
+ [InlineData("https://www.twitch.tv/streamer8/clip/FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf", "FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf")]
+ [InlineData("https://www.twitch.tv/streamer8/clip/SpineyPieTwitchRPGNurturing?featured=false&filter=clips&range=all&sort=time", "SpineyPieTwitchRPGNurturing")]
+ [InlineData("https://www.twitch.tv/streamer8/clip/FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf?featured=false&filter=clips&range=all&sort=time", "FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf")]
+ [InlineData("https://clips.twitch.tv/SpineyPieTwitchRPGNurturing", "SpineyPieTwitchRPGNurturing")]
+ [InlineData("https://clips.twitch.tv/FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf", "FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf")]
+ [InlineData("https://clips.twitch.tv/SpineyPieTwitchRPGNurturing/", "SpineyPieTwitchRPGNurturing")]
+ [InlineData("https://clips.twitch.tv/FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf/", "FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf")]
+ public void CorrectlyParsesClipLink(string link, string expectedId)
+ {
+ var match = IdParse.MatchClipId(link);
+
+ Assert.NotNull(match);
+ Assert.Equal(expectedId, match.Value);
+ }
+
+ [Theory]
+ [InlineData("41546181")] // Oldest VODs - 8
+ [InlineData("982306410")] // Old VODs - 9
+ [InlineData("6834869128")] // Current VODs - 10
+ [InlineData("11987163407")] // Future VODs - 11
+ [InlineData("SpineyPieTwitchRPGNurturing")]
+ [InlineData("FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf")]
+ public void CorrectlyParsesVodOrClipId(string id)
+ {
+ var match = IdParse.MatchVideoOrClipId(id);
+
+ Assert.NotNull(match);
+ Assert.Equal(id, match.Value);
+ }
+
+ [Theory]
+ [InlineData("https://www.twitch.tv/videos/41546181", "41546181")] // Oldest VODs - 8
+ [InlineData("https://www.twitch.tv/videos/982306410", "982306410")] // Old VODs - 9
+ [InlineData("https://www.twitch.tv/videos/6834869128", "6834869128")] // Current VODs - 10
+ [InlineData("https://www.twitch.tv/videos/11987163407", "11987163407")] // Future VODs - 11
+ [InlineData("https://www.twitch.tv/kitboga/video/2865132173", "2865132173")] // Alternate highlight URL
+ [InlineData("https://www.twitch.tv/kitboga/v/2865132173", "2865132173")] // Alternate VOD URL
+ [InlineData("https://www.twitch.tv/videos/4894164023/", "4894164023")]
+ [InlineData("https://www.twitch.tv/streamer8/clip/SpineyPieTwitchRPGNurturing", "SpineyPieTwitchRPGNurturing")]
+ [InlineData("https://www.twitch.tv/streamer8/clip/FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf", "FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf")]
+ [InlineData("https://www.twitch.tv/streamer8/clip/SpineyPieTwitchRPGNurturing?featured=false&filter=clips&range=all&sort=time", "SpineyPieTwitchRPGNurturing")]
+ [InlineData("https://www.twitch.tv/streamer8/clip/FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf?featured=false&filter=clips&range=all&sort=time", "FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf")]
+ [InlineData("https://clips.twitch.tv/SpineyPieTwitchRPGNurturing", "SpineyPieTwitchRPGNurturing")]
+ [InlineData("https://clips.twitch.tv/FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf", "FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf")]
+ [InlineData("https://clips.twitch.tv/SpineyPieTwitchRPGNurturing/", "SpineyPieTwitchRPGNurturing")]
+ [InlineData("https://clips.twitch.tv/FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf/", "FuriousFlaccidTireArgieB8-NHbTiYQlzwHVvv_Vf")]
+ public void CorrectlyParsesVodOrClipLink(string link, string expectedId)
+ {
+ var match = IdParse.MatchVideoOrClipId(link);
+
+ Assert.NotNull(match);
+ Assert.Equal(expectedId, match.Value);
+ }
+
+ [Fact]
+ public void DoesNotParseGarbageVodId()
+ {
+ const string GARBAGE = "SORRY FOR THE TRAFFIC NaM";
+
+ var match = IdParse.MatchVideoId(GARBAGE);
+
+ Assert.Null(match);
+ }
+
+ [Fact]
+ public void DoesNotParseGarbageClipId()
+ {
+ const string GARBAGE = "SORRY FOR THE TRAFFIC NaM";
+
+ var match = IdParse.MatchClipId(GARBAGE);
+
+ Assert.Null(match);
+ }
+
+ [Fact]
+ public void DoesNotParseGarbageVodOrClipId()
+ {
+ const string GARBAGE = "SORRY FOR THE TRAFFIC NaM";
+
+ var match = IdParse.MatchVideoOrClipId(GARBAGE);
+
+ Assert.Null(match);
+ }
+ }
+}
\ No newline at end of file
diff --git a/TwitchDownloaderCore/Tools/IdParse.cs b/TwitchDownloaderCore/Tools/IdParse.cs
new file mode 100644
index 00000000..485f4efc
--- /dev/null
+++ b/TwitchDownloaderCore/Tools/IdParse.cs
@@ -0,0 +1,71 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace TwitchDownloaderCore.Tools
+{
+ public static class IdParse
+ {
+ // TODO: Use source generators when .NET7
+ private static readonly Regex VideoId = new(@"(?<=^|twitch\.tv\/videos\/)\d+(?=\/?(?:$|\?))", RegexOptions.Compiled);
+ private static readonly Regex HighlightId = new(@"(?<=^|twitch\.tv\/\w+\/v(?:ideo)?\/)\d+(?=\/?(?:$|\?))", RegexOptions.Compiled);
+ private static readonly Regex ClipId = new(@"(?<=^|(?:clips\.)?twitch\.tv\/(?:\w+\/clip\/)?)[\w-]+?(?=\/?(?:$|\?))", RegexOptions.Compiled);
+
+ /// A of the video's id or .
+ [return: MaybeNull]
+ public static Match MatchVideoId(string text)
+ {
+ text = text.Trim();
+
+ 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 .
+ [return: MaybeNull]
+ public static Match MatchClipId(string text)
+ {
+ text = text.Trim();
+
+ 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 .
+ [return: MaybeNull]
+ public static Match MatchVideoOrClipId(string text)
+ {
+ text = text.Trim();
+
+ 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/TwitchDownloaderCore/Tools/TwitchRegex.cs b/TwitchDownloaderCore/Tools/TwitchRegex.cs
index fa5f718c..6f2e4f8b 100644
--- a/TwitchDownloaderCore/Tools/TwitchRegex.cs
+++ b/TwitchDownloaderCore/Tools/TwitchRegex.cs
@@ -1,66 +1,12 @@
-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+\/v(?:ideo)?\/)\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);
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);
-
- /// 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 e6913302..9fcf5861 100644
--- a/TwitchDownloaderWPF/PageChatDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageChatDownload.xaml.cs
@@ -225,7 +225,7 @@ private void UpdateActionButtons(bool isDownloading)
public static string ValidateUrl(string text)
{
- var vodClipIdMatch = TwitchRegex.MatchVideoOrClipId(text);
+ var vodClipIdMatch = IdParse.MatchVideoOrClipId(text);
return vodClipIdMatch is { Success: true }
? vodClipIdMatch.Value
: null;
diff --git a/TwitchDownloaderWPF/PageClipDownload.xaml.cs b/TwitchDownloaderWPF/PageClipDownload.xaml.cs
index 06b7770d..596e23cb 100644
--- a/TwitchDownloaderWPF/PageClipDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageClipDownload.xaml.cs
@@ -113,7 +113,7 @@ private void UpdateActionButtons(bool isDownloading)
private static string ValidateUrl(string text)
{
- var clipIdMatch = TwitchRegex.MatchClipId(text);
+ var clipIdMatch = IdParse.MatchClipId(text);
return clipIdMatch is { Success: true }
? clipIdMatch.Value
: null;
diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
index bb8732e1..1202a5e2 100644
--- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
@@ -285,7 +285,7 @@ public void SetImage(string imageUri, bool isGif)
private static long ValidateUrl(string text)
{
- var vodIdMatch = TwitchRegex.MatchVideoId(text);
+ var vodIdMatch = IdParse.MatchVideoId(text);
if (vodIdMatch is {Success: true} && long.TryParse(vodIdMatch.ValueSpan, out var vodId))
{
return vodId;