Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve ID parsing #1158

Merged
merged 4 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TwitchDownloaderCLI/Modes/DownloadChat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
2 changes: 1 addition & 1 deletion TwitchDownloaderCLI/Modes/DownloadClip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
2 changes: 1 addition & 1 deletion TwitchDownloaderCLI/Modes/DownloadVideo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
134 changes: 134 additions & 0 deletions TwitchDownloaderCore.Tests/ToolTests/IdParseTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
71 changes: 71 additions & 0 deletions TwitchDownloaderCore/Tools/IdParse.cs
Original file line number Diff line number Diff line change
@@ -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);

/// <returns>A <see cref="Match"/> of the video's id or <see langword="null"/>.</returns>
[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;
}

/// <returns>A <see cref="Match"/> of the clip's id or <see langword="null"/>.</returns>
[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;
}

/// <returns>A <see cref="Match"/> of the video/clip's id or <see langword="null"/>.</returns>
[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;
}
}
}
54 changes: 0 additions & 54 deletions TwitchDownloaderCore/Tools/TwitchRegex.cs
Original file line number Diff line number Diff line change
@@ -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);

/// <returns>A <see cref="Match"/> of the video's id or <see langword="null"/>.</returns>
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;
}

/// <returns>A <see cref="Match"/> of the clip's id or <see langword="null"/>.</returns>
public static Match MatchClipId(string text)
{
var clipIdMatch = ClipId.Match(text);
if (clipIdMatch.Success && !clipIdMatch.Value.All(char.IsDigit))
{
return clipIdMatch;
}

return null;
}

/// <returns>A <see cref="Match"/> of the video/clip's id or <see langword="null"/>.</returns>
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;
}
}
}
2 changes: 1 addition & 1 deletion TwitchDownloaderWPF/PageChatDownload.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion TwitchDownloaderWPF/PageClipDownload.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion TwitchDownloaderWPF/PageVodDownload.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading