Skip to content

Commit

Permalink
Support properly rendering Subtember gift subs (#1237)
Browse files Browse the repository at this point in the history
* Extract common icon sizing into property

* Return empty bitmap instead of null on failure to find a corresponding icon

* Allow Twitch to gift anonymous subs
This happens whenever Twitch adds additional sub gifts when a user gifts subs anonymously

* Correctly render Twitch gifting additional subs during Subtember

* Add tests
  • Loading branch information
ScrubN authored Oct 28, 2024
1 parent e272112 commit b2bbfea
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 14 deletions.
35 changes: 27 additions & 8 deletions TwitchDownloaderCore.Tests/ToolTests/HighlightIconsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ private static Comment CreateCommentWithMessage(string viewerDisplayName, string
[InlineData(
"{\"body\":\"viewer8 is gifting 5 Tier 1 Subs to streamer8's community! They've gifted a total of 349 in the channel! \",\"bits_spent\":0,\"fragments\":[{\"text\":\"viewer8 is gifting 5 Tier 1 Subs to streamer8's community! They've gifted a total of 349 in the channel! \",\"emoticon\":null}],\"user_badges\":[{\"_id\":\"subscriber\",\"version\":\"6\"},{\"_id\":\"bits\",\"version\":\"50000\"}],\"user_color\":\"#DAA520\",\"emoticons\":[]}",
HighlightType.GiftedMany)]
// +Special case in separate method.
// GiftedSingle
[InlineData(
"{\"body\":\"viewer8 gifted a Tier 1 sub to viewer9! \",\"bits_spent\":0,\"fragments\":[{\"text\":\"viewer8 gifted a Tier 1 sub to viewer9! \",\"emoticon\":null}],\"user_badges\":[{\"_id\":\"subscriber\",\"version\":\"6\"},{\"_id\":\"bits\",\"version\":\"50000\"}],\"user_color\":\"#DAA520\",\"emoticons\":[]}",
Expand Down Expand Up @@ -153,17 +154,35 @@ public void CorrectlyIdentifiesHighlightTypes(string messageString, HighlightTyp
Assert.Equal(expectedType, actualType);
}

[Fact]
public void CorrectlyIdentifiesAnonymousGiftSub()
[Theory]
[InlineData("{\"display_name\":\"Twitch\",\"_id\":\"12826\",\"name\":\"twitch\",\"bio\":\"Twitch is where thousands of communities come together for whatever, every day. \",\"created_at\":\"2007-05-22T10:39:54.238271Z\",\"updated_at\":\"2024-09-22T22:28:39.594659Z\",\"logo\":\"https://static-cdn.jtvnw.net/jtv_user_pictures/aa88230d-7af5-4053-a7cd-889e626d3382-profile_image-300x300.png\"}",
"{\"body\":\"We added 13 Gift Subs AND 10 Bonus Gift Subs to viewer8's gift! \",\"bits_spent\":0,\"fragments\":[{\"text\":\"We added 13 Gift Subs AND 10 Bonus Gift Subs to viewer8's gift! \",\"emoticon\":null}],\"user_badges\":[],\"user_color\":null,\"emoticons\":[]}")]
[InlineData("{\"display_name\":\"Twitch\",\"_id\":\"12826\",\"name\":\"twitch\",\"bio\":\"Twitch is where thousands of communities come together for whatever, every day. \",\"created_at\":\"2007-05-22T10:39:54.238271Z\",\"updated_at\":\"2024-09-22T22:28:39.594659Z\",\"logo\":\"https://static-cdn.jtvnw.net/jtv_user_pictures/aa88230d-7af5-4053-a7cd-889e626d3382-profile_image-300x300.png\"}",
"{\"body\":\"We added 1 Gift Subs to viewer8's gift! \",\"bits_spent\":0,\"fragments\":[{\"text\":\"We added 1 Gift Subs to viewer8's gift! \",\"emoticon\":null}],\"user_badges\":[],\"user_color\":null,\"emoticons\":[]}")]
public void CorrectlyIdentifiesSubtemberGiftedMany(string commenterString, string messageString)
{
const HighlightType EXPECTED_TYPE = HighlightType.GiftedMany;

var commenter = JsonSerializer.Deserialize<Commenter>(commenterString)!;
var message = JsonSerializer.Deserialize<Message>(messageString)!;
var comment = CreateCommentWithCommenterAndMessage(commenter, message);

var actualType = HighlightIcons.GetHighlightType(comment);

Assert.Equal(EXPECTED_TYPE, actualType);
}

[Theory]
[InlineData("{\"display_name\":\"AnAnonymousGifter\",\"_id\":\"274598607\",\"name\":\"ananonymousgifter\",\"type\":\"user\",\"bio\":\"?????????????????????????????\",\"created_at\":\"2018-11-12T21:57:31.811529Z\",\"updated_at\":\"2022-04-18T21:57:27.392173Z\",\"logo\":\"https://static-cdn.jtvnw.net/jtv_user_pictures/ae7b05c6-c924-44ab-8203-475a2d3e488c-profile_image-300x300.png\"}",
"{\"body\":\"An anonymous user gifted a Tier 1 sub to viewer8! \",\"bits_spent\":0,\"fragments\":[{\"text\":\"An anonymous user gifted a Tier 1 sub to viewer8! \",\"emoticon\":null}],\"user_badges\":[],\"user_color\":null,\"emoticons\":[]}")]
[InlineData("{\"display_name\":\"Twitch\",\"_id\":\"12826\",\"name\":\"twitch\",\"bio\":\"Twitch is where thousands of communities come together for whatever, every day. \",\"created_at\":\"2007-05-22T10:39:54.238271Z\",\"updated_at\":\"2024-09-22T22:28:39.594659Z\",\"logo\":\"https://static-cdn.jtvnw.net/jtv_user_pictures/aa88230d-7af5-4053-a7cd-889e626d3382-profile_image-300x300.png\"}",
"{\"body\":\"An anonymous user gifted a Tier 1 sub to viewer8! \",\"bits_spent\":0,\"fragments\":[{\"text\":\"An anonymous user gifted a Tier 1 sub to viewer8! \",\"emoticon\":null}],\"user_badges\":[],\"user_color\":null,\"emoticons\":[]}")]
public void CorrectlyIdentifiesAnonymousGiftSub(string commenterString, string messageString)
{
const string COMMENTER_STRING =
"{\"display_name\":\"AnAnonymousGifter\",\"_id\":\"274598607\",\"name\":\"ananonymousgifter\",\"type\":\"user\",\"bio\":\"?????????????????????????????\",\"created_at\":\"2018-11-12T21:57:31.811529Z\",\"updated_at\":\"2022-04-18T21:57:27.392173Z\",\"logo\":\"https://static-cdn.jtvnw.net/jtv_user_pictures/ae7b05c6-c924-44ab-8203-475a2d3e488c-profile_image-300x300.png\"}";
const string MESSAGE_STRING =
"{\"body\":\"An anonymous user gifted a Tier 1 sub to viewer8! \",\"bits_spent\":0,\"fragments\":[{\"text\":\"An anonymous user gifted a Tier 1 sub to viewer8! \",\"emoticon\":null}],\"user_badges\":[],\"user_color\":null,\"emoticons\":[]}";
const HighlightType EXPECTED_TYPE = HighlightType.GiftedAnonymous;

var commenter = JsonSerializer.Deserialize<Commenter>(COMMENTER_STRING)!;
var message = JsonSerializer.Deserialize<Message>(MESSAGE_STRING)!;
var commenter = JsonSerializer.Deserialize<Commenter>(commenterString)!;
var message = JsonSerializer.Deserialize<Message>(messageString)!;
var comment = CreateCommentWithCommenterAndMessage(commenter, message);

var actualType = HighlightIcons.GetHighlightType(comment);
Expand Down
2 changes: 1 addition & 1 deletion TwitchDownloaderCore/ChatRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ private void DrawAccentedMessage(Comment comment, List<(SKImageInfo info, SKBitm
Point iconPoint = new()
{
X = drawPos.X,
Y = (int)((renderOptions.SectionHeight - highlightIcon?.Height) / 2.0 ?? 0)
Y = (int)((renderOptions.SectionHeight - highlightIcon.Height) / 2.0)
};

switch (highlightType)
Expand Down
22 changes: 17 additions & 5 deletions TwitchDownloaderCore/Tools/HighlightIcons.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public sealed class HighlightIcons : IDisposable
{
public bool Disposed { get; private set; }

private double FinalIconSize => _fontSize / 0.6; // 20x20px @ 12pt font

private const string SUBSCRIBED_TIER_ICON_SVG = "m 32.599229,13.144498 c 1.307494,-2.80819 5.494049,-2.80819 6.80154,0 l 5.648628,12.140919 13.52579,1.877494 c 3.00144,0.418654 4.244522,3.893468 2.138363,5.967405 -3.357829,3.309501 -6.715662,6.618992 -10.073491,9.928491 L 53.07148,56.81637 c 0.524928,2.962772 -2.821092,5.162303 -5.545572,3.645496 L 36,54.043603 24.474093,60.461866 C 21.749613,61.975455 18.403591,59.779142 18.92852,56.81637 L 21.359942,43.058807 11.286449,33.130316 c -2.1061588,-2.073937 -0.863074,-5.548751 2.138363,-5.967405 l 13.52579,-1.877494 z";
private const string SUBSCRIBED_PRIME_ICON_SVG = "m 61.894653,21.663055 v 25.89488 c 0,3.575336 -2.898361,6.47372 -6.473664,6.47372 H 16.57901 c -3.573827,-0.0036 -6.470094,-2.89986 -6.473663,-6.47372 V 21.663055 L 23.052674,31.373635 36,18.426194 c 4.315772,4.315816 8.631553,8.631629 12.947323,12.947441 z";
private const string GIFTED_SINGLE_ICON_SVG = "m 55.187956,23.24523 h 6.395987 V 42.433089 H 58.38595 V 61.620947 H 13.614042 V 42.433089 H 10.416049 V 23.24523 h 6.395987 v -3.859957 c 0,-8.017328 9.689919,-12.0307888 15.359963,-6.363975 0.418936,0.418935 0.796298,0.879444 1.125692,1.371934 l 2.702305,4.055034 2.702305,-4.055034 a 8.9863623,8.9863139 0 0 1 1.125692,-1.371934 c 5.666845,-5.6668138 15.359963,-1.653353 15.359963,6.363975 z M 23.208023,19.385273 v 3.859957 h 8.301992 l -3.536982,-5.305444 a 2.6031666,2.6031528 0 0 0 -4.76501,1.445487 z m 25.583946,0 v 3.859957 h -8.301991 l 3.536983,-5.305444 a 2.6031666,2.6031528 0 0 1 4.765008,1.442286 z m 6.395987,10.255909 v 6.395951 H 39.19799 v -6.395951 z m -3.197992,25.58381 V 42.433089 H 39.19799 V 55.224992 Z M 32.802003,29.641182 v 6.395951 H 16.812036 v -6.395951 z m 0,12.791907 H 20.010028 v 12.791903 h 12.791975 z";
Expand All @@ -41,6 +43,7 @@ public sealed class HighlightIcons : IDisposable
private const string WATCH_STREAK_ICON_SVG = "M 38.84325,21.169078 33.156748,14.060989 21.215093,27.992844 a 21.267516,21.267402 0 0 0 -5.11785,13.846557 c 0,9.752298 7.961102,17.713358 17.713453,17.713358 H 38.50206 A 17.400696,17.400602 0 0 0 55.902755,42.152157 c 0,-5.288419 -1.848114,-10.406242 -5.231581,-14.500501 L 41.686501,16.904225 Z m -13.306415,10.519973 7.619913,-9.098354 5.686502,7.108089 2.843251,-4.264854 4.606066,5.885497 a 16.945776,16.945684 0 0 1 3.923686,10.832728 c 0,5.91393 -4.407039,10.804296 -10.121973,11.600401 1.02357,-1.336321 1.592221,-2.985397 1.592221,-4.719771 0,-1.478483 -0.511786,-2.900101 -1.421626,-4.065827 l -4.264877,-5.316851 -4.264876,5.316851 c -0.90984,1.137294 -1.421625,2.587344 -1.421625,4.065827 0,1.705941 0.56865,3.355018 1.535355,4.662906 A 12.026952,12.026887 0 0 1 21.783744,41.839401 c 0,-3.72464 1.336328,-7.335548 3.753091,-10.15035 z";
private const string CHARITY_DONATION_ICON_SVG = "M 14.211579,29.774743 23.549474,11.09897 H 48.450526 L 57.788421,29.774743 47.345541,42.829108 60.901052,60.90103 H 39.112633 L 36,57.010242 32.887368,60.90103 h -21.78842 l 13.55551,-18.071922 z m 13.185107,-12.450515 -3.112631,6.225256 h 23.43189 l -3.112632,-6.225256 z m 2.378051,12.450515 2.334473,3.112628 -3.598202,4.796559 -6.32798,-7.909187 z m 10.20943,22.255295 2.119703,2.645734 h 6.346656 l -5.12028,-6.829109 -3.342966,4.180262 z M 23.549474,54.675772 42.225261,29.774743 h 7.59171 L 29.89613,54.675772 Z";
private const string CHANNEL_POINT_ICON_SVG = "m 34.074833,10.317667 a 25.759205,25.759174 0 0 0 -23.83413,25.686052 25.759298,25.759267 0 0 0 51.518594,0 25.759205,25.759174 0 0 0 -27.684464,-25.686052 z m 0.329458,6.432744 a 19.319404,19.319381 0 0 1 20.915597,19.253308 19.319888,19.319865 0 0 1 -38.639776,0 19.319404,19.319381 0 0 1 17.724179,-19.253308 z M 36,23.124918 v 6.439401 a 6.4398012,6.4397935 0 0 1 6.439407,6.4394 H 48.88048 A 12.879602,12.879587 0 0 0 36,23.124918 Z";
private const string BLANK_ICON_SVG = " "; // A single space is enough to pass the SVG parse and get an empty path

private const int ICON_SIZE = 72; // Icon SVG strings are scaled for 72x72

Expand All @@ -56,6 +59,7 @@ public sealed class HighlightIcons : IDisposable
private SKImage _bitBadgeTierNotificationIcon;
private SKImage _watchStreakIcon;
private SKImage _charityDonationIcon;
private SKImage _blankIcon;

private readonly DirectoryInfo _cacheDir;
private readonly SKColor _purple;
Expand All @@ -75,7 +79,7 @@ public HighlightIcons(ChatRenderOptions renderOptions, SKColor iconPurple, SKPai
if (_outline)
{
_outlinePaint = outlinePaint.Clone();
_outlinePaint.StrokeWidth *= (float)(ICON_SIZE / (_fontSize / 0.6));
_outlinePaint.StrokeWidth *= (float)(ICON_SIZE / FinalIconSize);
}
}

Expand Down Expand Up @@ -151,10 +155,18 @@ public static HighlightType GetHighlightType(Comment comment)
return HighlightType.Raid;
}

const string TWITCH_ACCOUNT_ID = "12826";
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 or TWITCH_ACCOUNT_ID && GiftAnonymousRegex.IsMatch(comment.message.body))
return HighlightType.GiftedAnonymous;

if (comment.commenter._id is TWITCH_ACCOUNT_ID && comment.message.body.EndsWith("'s gift! ") &&
Regex.IsMatch(comment.message.body, @"^We added \d+ Gift Subs (?:AND \d+ Bonus Gift Subs )?to "))
{
// TODO: Make a dedicated enum value for Subtember?
return HighlightType.GiftedMany;
}

return HighlightType.None;
}

Expand All @@ -172,14 +184,14 @@ public SKImage GetHighlightIcon(HighlightType highlightType, SKColor textColor)
HighlightType.BitBadgeTierNotification => _bitBadgeTierNotificationIcon ??= GenerateSvgIcon(BIT_BADGE_TIER_NOTIFICATION_ICON_SVG, textColor),
HighlightType.WatchStreak => _watchStreakIcon ??= GenerateSvgIcon(WATCH_STREAK_ICON_SVG, textColor),
HighlightType.CharityDonation => _charityDonationIcon ??= GenerateSvgIcon(CHARITY_DONATION_ICON_SVG, textColor),
_ => null
_ => _blankIcon ??= GenerateSvgIcon(BLANK_ICON_SVG, textColor)
};
}

private SKImage GenerateGiftedManyIcon()
{
//int newSize = (int)(fontSize / 0.2727); // 44*44px @ 12pt font // Doesn't work because our image sections aren't tall enough and I'm not rewriting that right now
var finalIconSize = (int)(_fontSize / 0.6); // 20x20px @ 12pt font
var finalIconSize = (int)FinalIconSize;

if (_offline)
{
Expand Down Expand Up @@ -218,7 +230,7 @@ private SKImage GenerateSvgIcon(string iconSvgString, SKColor iconColor)
}

tempCanvas.DrawPath(iconPath, iconPaint);
var newSize = (int)(_fontSize / 0.6); // 20*20px @ 12pt font
var newSize = (int)FinalIconSize;
var imageInfo = new SKImageInfo(newSize, newSize);
var resizedBitmap = tempBitmap.Resize(imageInfo, SKFilterQuality.High);

Expand Down

0 comments on commit b2bbfea

Please sign in to comment.