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;