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

Ability to embed twitch badges and bit emotes #437

Merged
merged 10 commits into from
Nov 20, 2022
36 changes: 35 additions & 1 deletion TwitchDownloaderCore/ChatDownloader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using NeoSmart.Unicode;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SkiaSharp;
using System;
Expand Down Expand Up @@ -333,13 +334,19 @@ public async Task DownloadAsync(IProgress<ProgressReport> progress, Cancellation
chatRoot.emotes = new Emotes();
List<EmbedEmoteData> firstParty = new List<EmbedEmoteData>();
List<EmbedEmoteData> thirdParty = new List<EmbedEmoteData>();
List<EmbedChatBadge> badges = new List<EmbedChatBadge>();
List<EmbedCheerEmote> bits = new List<EmbedCheerEmote>();

string cacheFolder = Path.Combine(Path.GetTempPath(), "TwitchDownloader", "cache");
List<TwitchEmote> thirdPartyEmotes = new List<TwitchEmote>();
List<TwitchEmote> firstPartyEmotes = new List<TwitchEmote>();
List<ChatBadge> twitchBadges = new List<ChatBadge>();
List<CheerEmote> twitchBits = new List<CheerEmote>();

thirdPartyEmotes = await TwitchHelper.GetThirdPartyEmotes(chatRoot.streamer.id, cacheFolder, bttv: downloadOptions.BttvEmotes, ffz: downloadOptions.FfzEmotes, stv: downloadOptions.StvEmotes);
firstPartyEmotes = await TwitchHelper.GetEmotes(comments, cacheFolder);
twitchBadges = await TwitchHelper.GetChatBadges(chatRoot.streamer.id, cacheFolder);
twitchBits = await TwitchHelper.GetBits(cacheFolder, chatRoot.streamer.id.ToString());

foreach (TwitchEmote emote in thirdPartyEmotes)
{
Expand All @@ -362,9 +369,36 @@ public async Task DownloadAsync(IProgress<ProgressReport> progress, Cancellation
newEmote.height = emote.Height / emote.ImageScale;
firstParty.Add(newEmote);
}
foreach (ChatBadge badge in twitchBadges)
{
EmbedChatBadge newBadge = new EmbedChatBadge();
newBadge.name = badge.Name;
newBadge.versions = badge.VersionsData;
badges.Add(newBadge);
}
foreach (CheerEmote bit in twitchBits)
{
EmbedCheerEmote newBit = new EmbedCheerEmote();
newBit.prefix = bit.prefix;
newBit.tierList = new Dictionary<int, EmbedEmoteData>();
foreach (KeyValuePair<int, TwitchEmote> emotePair in bit.tierList)
{
EmbedEmoteData newEmote = new EmbedEmoteData();
newEmote.id = emotePair.Value.Id;
newEmote.imageScale = emotePair.Value.ImageScale;
newEmote.data = emotePair.Value.ImageData;
newEmote.name = emotePair.Value.Name;
newEmote.width = emotePair.Value.Width / emotePair.Value.ImageScale;
newEmote.height = emotePair.Value.Height / emotePair.Value.ImageScale;
newBit.tierList.Add(emotePair.Key, newEmote);
}
bits.Add(newBit);
}

chatRoot.emotes.thirdParty = thirdParty;
chatRoot.emotes.firstParty = firstParty;
chatRoot.emotes.twitchBadges = badges;
chatRoot.emotes.twitchBits = bits;
}

if (downloadOptions.DownloadFormat == DownloadFormat.Json)
Expand Down
4 changes: 2 additions & 2 deletions TwitchDownloaderCore/ChatRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ public ChatRenderer(ChatRenderOptions chatRenderOptions)
public async Task RenderVideoAsync(IProgress<ProgressReport> progress, CancellationToken cancellationToken)
{
progress.Report(new ProgressReport() { reportType = ReportType.Message, data = "Fetching Images" });
Task<List<ChatBadge>> badgeTask = Task.Run(() => TwitchHelper.GetChatBadges(chatRoot.streamer.id, renderOptions.TempFolder));
Task<List<ChatBadge>> badgeTask = Task.Run(() => TwitchHelper.GetChatBadges(chatRoot.streamer.id, renderOptions.TempFolder, chatRoot.emotes));
Task<List<TwitchEmote>> emoteTask = Task.Run(() => TwitchHelper.GetEmotes(chatRoot.comments, renderOptions.TempFolder, chatRoot.emotes));
Task<List<TwitchEmote>> emoteThirdTask = Task.Run(() => TwitchHelper.GetThirdPartyEmotes(chatRoot.streamer.id, renderOptions.TempFolder, chatRoot.emotes, renderOptions.BttvEmotes, renderOptions.FfzEmotes, renderOptions.StvEmotes));
Task<List<CheerEmote>> cheerTask = Task.Run(() => TwitchHelper.GetBits(renderOptions.TempFolder, chatRoot.streamer.id.ToString()));
Task<List<CheerEmote>> cheerTask = Task.Run(() => TwitchHelper.GetBits(renderOptions.TempFolder, chatRoot.streamer.id.ToString(), chatRoot.emotes));
Task<Dictionary<string, SKBitmap>> emojiTask = Task.Run(() => TwitchHelper.GetTwitterEmojis(renderOptions.TempFolder));

await Task.WhenAll(badgeTask, emoteTask, emoteThirdTask, cheerTask, emojiTask);
Expand Down
85 changes: 65 additions & 20 deletions TwitchDownloaderCore/TwitchHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using NeoSmart.Unicode;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SkiaSharp;
using System;
Expand All @@ -10,6 +11,7 @@
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using TwitchDownloaderCore.Properties;
using TwitchDownloaderCore.TwitchObjects;

Expand Down Expand Up @@ -226,13 +228,7 @@ public static async Task<List<TwitchEmote>> GetThirdPartyEmotes(int streamerId,
List<TwitchEmote> returnList = new List<TwitchEmote>();
List<string> alreadyAdded = new List<string>();

string bttvFolder = Path.Combine(cacheFolder, "bttv");
string ffzFolder = Path.Combine(cacheFolder, "ffz");
string stvFolder = Path.Combine(cacheFolder, "stv");

EmoteResponse emoteDataResponse = await GetThirdPartyEmoteData(streamerId.ToString(), bttv, ffz, stv);

if (embededEmotes != null)
if (embededEmotes != null && embededEmotes.thirdParty != null)
{
foreach (EmbedEmoteData emoteData in embededEmotes.thirdParty)
{
Expand All @@ -246,6 +242,15 @@ public static async Task<List<TwitchEmote>> GetThirdPartyEmotes(int streamerId,
}
}

// TODO: RETURN HERE IF IN OFFLINE MODE!
return returnList;

string bttvFolder = Path.Combine(cacheFolder, "bttv");
string ffzFolder = Path.Combine(cacheFolder, "ffz");
string stvFolder = Path.Combine(cacheFolder, "stv");

EmoteResponse emoteDataResponse = await GetThirdPartyEmoteData(streamerId.ToString(), bttv, ffz, stv);

if (bttv)
{
if (!Directory.Exists(bttvFolder))
Expand Down Expand Up @@ -308,7 +313,7 @@ public static async Task<List<TwitchEmote>> GetEmotes(List<Comment> comments, st
if (!Directory.Exists(emoteFolder))
TwitchHelper.CreateDirectory(emoteFolder);

if (embededEmotes != null)
if (embededEmotes != null && embededEmotes.firstParty != null)
{
foreach (EmbedEmoteData emoteData in embededEmotes.firstParty)
{
Expand All @@ -322,6 +327,9 @@ public static async Task<List<TwitchEmote>> GetEmotes(List<Comment> comments, st
}
}

// TODO: RETURN HERE IF IN OFFLINE MODE!
return returnList;

foreach (var comment in comments)
{
if (comment.message.fragments == null)
Expand Down Expand Up @@ -353,10 +361,26 @@ public static async Task<List<TwitchEmote>> GetEmotes(List<Comment> comments, st
return returnList;
}

public static async Task<List<ChatBadge>> GetChatBadges(int streamerId, string cacheFolder)
public static async Task<List<ChatBadge>> GetChatBadges(int streamerId, string cacheFolder, Emotes embededEmotes = null)
{
List<ChatBadge> returnList = new List<ChatBadge>();
List<string> alreadyAdded = new List<string>();

if (embededEmotes != null && embededEmotes.twitchBadges != null)
{
foreach (EmbedChatBadge data in embededEmotes.twitchBadges)
{
ChatBadge newBadge = new ChatBadge(data.name, data.versions);
returnList.Add(newBadge);
alreadyAdded.Add(data.name);
}
}

// TODO: RETURN HERE IF IN OFFLINE MODE!
return returnList;

// TODO: this currently only does twitch badges, but we could also support FFZ, BTTV, 7TV, etc badges!
// TODO: would want to make this configurable as we do for emotes though...
JObject globalBadges = JObject.Parse(await httpClient.GetStringAsync("https://badges.twitch.tv/v1/badges/global/display"));
JObject subBadges = JObject.Parse(await httpClient.GetStringAsync($"https://badges.twitch.tv/v1/badges/channels/{streamerId}/display"));

Expand All @@ -368,8 +392,10 @@ public static async Task<List<ChatBadge>> GetChatBadges(int streamerId, string c
{
JProperty jBadgeProperty = badge.ToObject<JProperty>();
string name = jBadgeProperty.Name;
Dictionary<string, SKBitmap> versions = new Dictionary<string, SKBitmap>();
if (alreadyAdded.Contains(name))
continue;

Dictionary<string, byte[]> versions = new Dictionary<string, byte[]>();
foreach (var version in badge.First["versions"])
{
JProperty jVersionProperty = version.ToObject<JProperty>();
Expand All @@ -381,11 +407,7 @@ public static async Task<List<ChatBadge>> GetChatBadges(int streamerId, string c
string[] id_parts = downloadUrl.Split('/');
string id = id_parts[id_parts.Length - 2];
byte[] bytes = await GetImage(badgeFolder, downloadUrl, id, "2", "png");
using MemoryStream ms = new MemoryStream(bytes);
//For some reason, twitch has corrupted images sometimes :) for example
//https://static-cdn.jtvnw.net/badges/v1/a9811799-dce3-475f-8feb-3745ad12b7ea/1
SKBitmap badgeImage = SKBitmap.Decode(ms);
versions.Add(versionString, badgeImage);
versions.Add(versionString, bytes);
}
catch (HttpRequestException)
{ }
Expand Down Expand Up @@ -442,9 +464,29 @@ public static async Task<List<ChatBadge>> GetChatBadges(int streamerId, string c
return returnCache;
}

public static async Task<List<CheerEmote>> GetBits(string cacheFolder, string channel_id = "")
public static async Task<List<CheerEmote>> GetBits(string cacheFolder, string channel_id = "", Emotes embededEmotes = null)
{
List<CheerEmote> returnCheermotes = new List<CheerEmote>();
List<CheerEmote> returnList = new List<CheerEmote>();
List<string> alreadyAdded = new List<string>();

if (embededEmotes != null && embededEmotes.twitchBits != null)
{
foreach (EmbedCheerEmote data in embededEmotes.twitchBits)
{
List<KeyValuePair<int, TwitchEmote>> tierList = new List<KeyValuePair<int, TwitchEmote>>();
CheerEmote newEmote = new CheerEmote() { prefix = data.prefix, tierList = tierList };
foreach (KeyValuePair<int, EmbedEmoteData> tier in data.tierList)
{
TwitchEmote tierEmote = new TwitchEmote(tier.Value.data, EmoteProvider.FirstParty, tier.Value.imageScale, tier.Value.id, tier.Value.name);
tierList.Add(new KeyValuePair<int, TwitchEmote>(tier.Key, tierEmote));
}
returnList.Add(newEmote);
alreadyAdded.Add(data.prefix);
}
}

// TODO: RETURN HERE IF IN OFFLINE MODE!
return returnList;

var request = new HttpRequestMessage()
{
Expand Down Expand Up @@ -484,6 +526,9 @@ public static async Task<List<CheerEmote>> GetBits(string cacheFolder, string ch
foreach (CheerNode node in group.nodes)
{
string prefix = node.prefix;
if (alreadyAdded.Contains(prefix))
continue;

List<KeyValuePair<int, TwitchEmote>> tierList = new List<KeyValuePair<int, TwitchEmote>>();
CheerEmote newEmote = new CheerEmote() { prefix = prefix, tierList = tierList };
foreach (Tier tier in node.tiers)
Expand All @@ -493,12 +538,12 @@ public static async Task<List<CheerEmote>> GetBits(string cacheFolder, string ch
TwitchEmote emote = new TwitchEmote(await GetImage(bitFolder, url, node.id + tier.bits, "2", "gif"), EmoteProvider.FirstParty, 2, prefix + minBits, prefix + minBits);
tierList.Add(new KeyValuePair<int, TwitchEmote>(minBits, emote));
}
returnCheermotes.Add(newEmote);
returnList.Add(newEmote);
}
}
}

return returnCheermotes;
return returnList;
}

public static DirectoryInfo CreateDirectory(string path)
Expand Down
21 changes: 18 additions & 3 deletions TwitchDownloaderCore/TwitchObjects/ChatBadge.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using SkiaSharp;
using NeoSmart.Unicode;
using Newtonsoft.Json.Linq;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;

namespace TwitchDownloaderCore.TwitchObjects
Expand All @@ -23,12 +27,23 @@ public class ChatBadge
{
public string Name;
public Dictionary<string, SKBitmap> Versions;
public Dictionary<string, byte[]> VersionsData;
public ChatBadgeType Type;

public ChatBadge(string name, Dictionary<string, SKBitmap> versions)
public ChatBadge(string name, Dictionary<string, byte[]> versions)
{
Name = name;
Versions = versions;
Versions = new Dictionary<string, SKBitmap>();
VersionsData = versions;

foreach (var version in versions)
{
using MemoryStream ms = new MemoryStream(version.Value);
//For some reason, twitch has corrupted images sometimes :) for example
//https://static-cdn.jtvnw.net/badges/v1/a9811799-dce3-475f-8feb-3745ad12b7ea/1
SKBitmap badgeImage = SKBitmap.Decode(ms);
Versions.Add(version.Key, badgeImage);
}

switch (name)
{
Expand Down
14 changes: 14 additions & 0 deletions TwitchDownloaderCore/TwitchObjects/ChatRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,24 @@ public class EmbedEmoteData
public int height { get; set; }
}

public class EmbedChatBadge
{
public string name { get; set; }
public Dictionary<string, byte[]> versions { get; set; }
}

public class EmbedCheerEmote
{
public string prefix { get; set; }
public Dictionary<int, EmbedEmoteData> tierList { get; set; }
}

public class Emotes
Copy link
Collaborator

@ScrubN ScrubN Nov 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe create new classes for embedded badges and bits

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doing that means no dealing with "upgrading" old chat files because the TryGetProperty would return false

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought on it a bit more. Keeping the single class could be nice IF we rename it to something more appropriate like "embeddedData". Still we do not need to deal with "upgrading" old jsons because we can TryGetProperty("embeddedData") else TryGetProperty("emotes").

We can also change --embed-emotes to --embed-data and add a simple find and replace to ConvertFromOldSyntax for backwards compatibility.

{
public List<EmbedEmoteData> thirdParty { get; set; }
public List<EmbedEmoteData> firstParty { get; set; }
public List<EmbedChatBadge> twitchBadges { get; set; }
public List<EmbedCheerEmote> twitchBits { get; set; }
}

public class CommentResponse
Expand Down