From 2d28ea6f2f1b0f5c344e6060477519626f59648d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=B3=E9=88=9E?= Date: Tue, 12 Dec 2023 05:15:56 +0800 Subject: [PATCH] Add DiscordService --- Helper.cs | 35 +- Json/SourceGenerationContext.cs | 8 +- LiveChatMonitorWorker.cs | 546 ++----- Models/Chat.cs | 1304 ++++++++--------- Models/Info.cs | 2063 +++++++++++++-------------- Program.cs | 1 + Services/DiscordService.cs | 329 +++++ Services/LiveChatDownloadService.cs | 2 - 8 files changed, 2170 insertions(+), 2118 deletions(-) create mode 100644 Services/DiscordService.cs diff --git a/Helper.cs b/Helper.cs index 3b277bc..95462bb 100644 --- a/Helper.cs +++ b/Helper.cs @@ -1,5 +1,4 @@ -using Discord; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; namespace YoutubeLiveChatToDiscord; @@ -36,38 +35,6 @@ where File.Exists(path) return YtdlPath; } - /// - /// 把.NET Core logger對應到Discord內建的logger上面 - /// - /// - /// - internal static Task DiscordWebhookClient_Log(LogMessage arg) - => Task.Run(() => - { - switch (arg.Severity) - { - case LogSeverity.Critical: - _logger.LogCritical("{message}", arg); - break; - case LogSeverity.Error: - _logger.LogError("{message}", arg); - break; - case LogSeverity.Warning: - _logger.LogWarning("{message}", arg); - break; - case LogSeverity.Info: - _logger.LogInformation("{message}", arg); - break; - case LogSeverity.Verbose: - _logger.LogTrace("{message}", arg); - break; - case LogSeverity.Debug: - default: - _logger.LogDebug("{message}", arg); - break; - } - }); - /// /// 處理Youtube的圖片url,取得原始尺寸圖片 /// diff --git a/Json/SourceGenerationContext.cs b/Json/SourceGenerationContext.cs index 0adb2e6..c11d3ff 100644 --- a/Json/SourceGenerationContext.cs +++ b/Json/SourceGenerationContext.cs @@ -4,7 +4,11 @@ // Must read: // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation?pivots=dotnet-8-0 -[JsonSerializable(typeof(Info))] -[JsonSerializable(typeof(Chat))] [JsonSourceGenerationOptions(WriteIndented = true, AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip)] +[JsonSerializable(typeof(Info.info))] +[JsonSerializable(typeof(Chat.chat))] +[JsonSerializable(typeof(Info.Thumbnail), TypeInfoPropertyName = "InfoThumbnail")] +[JsonSerializable(typeof(Chat.Thumbnail), TypeInfoPropertyName = "ChatThumbnail")] +[JsonSerializable(typeof(List), TypeInfoPropertyName = "ChatThumbnailList")] +[JsonSerializable(typeof(List), TypeInfoPropertyName = "InfoThumbnailList")] internal partial class SourceGenerationContext : JsonSerializerContext { } diff --git a/LiveChatMonitorWorker.cs b/LiveChatMonitorWorker.cs index 2d75178..b2fa4fe 100644 --- a/LiveChatMonitorWorker.cs +++ b/LiveChatMonitorWorker.cs @@ -1,451 +1,199 @@ -using Discord; -using Discord.Webhook; using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using YoutubeLiveChatToDiscord.Models; using YoutubeLiveChatToDiscord.Services; +using Chat = YoutubeLiveChatToDiscord.Models.Chat.chat; +using Info = YoutubeLiveChatToDiscord.Models.Info.info; -namespace YoutubeLiveChatToDiscord +namespace YoutubeLiveChatToDiscord; + +public class LiveChatMonitorWorker : BackgroundService { - public class LiveChatMonitorWorker : BackgroundService + private readonly ILogger _logger; + private readonly string _id; + private readonly FileInfo _liveChatFileInfo; + private long _position = 0; + private readonly LiveChatDownloadService _liveChatDownloadService; + private readonly DiscordService _discordService; + + public LiveChatMonitorWorker( + ILogger logger, + LiveChatDownloadService liveChatDownloadService, + DiscordService discordService + ) { - private readonly ILogger _logger; - private readonly string _id; - private readonly DiscordWebhookClient _client; - private readonly FileInfo _liveChatFileInfo; - private long _position = 0; - private readonly LiveChatDownloadService _liveChatDownloadService; - - public LiveChatMonitorWorker( - ILogger logger, - DiscordWebhookClient client, - LiveChatDownloadService liveChatDownloadService - ) - { - (_logger, _client, _liveChatDownloadService) = (logger, client, liveChatDownloadService); - _client.Log += Helper.DiscordWebhookClient_Log; + (_logger, _liveChatDownloadService, _discordService) = (logger, liveChatDownloadService, discordService); - _id = Environment.GetEnvironmentVariable("VIDEO_ID") ?? ""; - if (string.IsNullOrEmpty(_id)) throw new ArgumentException(nameof(_id)); + _id = Environment.GetEnvironmentVariable("VIDEO_ID") ?? ""; + if (string.IsNullOrEmpty(_id)) throw new ArgumentException(nameof(_id)); - _liveChatFileInfo = new($"{_id}.live_chat.json"); - } + _liveChatFileInfo = new($"{_id}.live_chat.json"); + } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try { - try + while (!stoppingToken.IsCancellationRequested) { - while (!stoppingToken.IsCancellationRequested) + if (_liveChatDownloadService.downloadProcess.IsCompleted) { - if (_liveChatDownloadService.downloadProcess.IsCompleted) - { - _ = _liveChatDownloadService.ExecuteAsync(stoppingToken) - .ContinueWith((_) => _logger.LogInformation("yt-dlp is stopped."), stoppingToken); - } - - _logger.LogInformation("Wait 10 seconds."); - await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); - _liveChatFileInfo.Refresh(); - - try - { - if (!_liveChatFileInfo.Exists) - { - throw new FileNotFoundException(null, _liveChatFileInfo.FullName); - } - - await Monitoring(stoppingToken); - } - catch (FileNotFoundException e) - { - _logger.LogWarning("Json file not found. {FileName}", e.FileName); - } + _ = _liveChatDownloadService.ExecuteAsync(stoppingToken) + .ContinueWith((_) => _logger.LogInformation("yt-dlp is stopped."), stoppingToken); } - } - catch (TaskCanceledException) { } - finally - { - _logger.LogError("Wait 10 seconds before closing the program. This is to prevent a restart loop from hanging the machine."); -#pragma warning disable CA2016 // 將 'CancellationToken' 參數轉送給方法 - await Task.Delay(TimeSpan.FromSeconds(10)); -#pragma warning restore CA2016 // 將 'CancellationToken' 參數轉送給方法 - } - } - - /// - /// Monitoring - /// - /// - /// - /// - private async Task Monitoring(CancellationToken stoppingToken) - { - await GetVideoInfo(stoppingToken); -#if !DEBUG - if (null == Environment.GetEnvironmentVariable("SKIP_STARTUP_WAITING")) - { - _logger.LogInformation("Wait 1 miunute to skip old chats"); - await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + _logger.LogInformation("Wait 10 seconds."); + await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); _liveChatFileInfo.Refresh(); - } -#endif - _position = _liveChatFileInfo.Length; - _logger.LogInformation("Start at position: {position}", _position); - _logger.LogInformation("Start Monitoring!"); - - while (!stoppingToken.IsCancellationRequested) - { - _liveChatFileInfo.Refresh(); - if (_liveChatFileInfo.Length > _position) - { - await ProcessChats(stoppingToken); - } - else if (_liveChatDownloadService.downloadProcess.IsCompleted) + try { - _logger.LogInformation("Download process is stopped. Restart monitoring."); - return; + if (!_liveChatFileInfo.Exists) + { + throw new FileNotFoundException(null, _liveChatFileInfo.FullName); + } + + await Monitoring(stoppingToken); } - else + catch (FileNotFoundException e) { - _position = _liveChatFileInfo.Length; - _logger.LogTrace("No new chat. Wait 10 seconds."); - // 每10秒檢查一次json檔 - await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); + _logger.LogWarning("Json file not found. {FileName}", e.FileName); } } } - - /// - /// GetVideoInfo - /// - /// - /// - /// - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = $"{nameof(SourceGenerationContext)} is used.")] - private async Task GetVideoInfo(CancellationToken stoppingToken) + catch (TaskCanceledException) { } + finally { - FileInfo videoInfo = new($"{_id}.info.json"); - if (!videoInfo.Exists) - { - // Chat json file 在 VideoInfo json file之後被產生,理論上這段不會進來 - throw new FileNotFoundException(null, videoInfo.FullName); - } - - Info? info = JsonSerializer.Deserialize( - await new StreamReader(videoInfo.OpenRead()).ReadToEndAsync(stoppingToken), - options: new() - { - TypeInfoResolver = SourceGenerationContext.Default - }); - string? Title = info?.title; - string? ChannelId = info?.channel_id; - string? thumb = info?.thumbnail; - - Environment.SetEnvironmentVariable("TITLE", Title); - Environment.SetEnvironmentVariable("CHANNEL_ID", ChannelId); - Environment.SetEnvironmentVariable("VIDEO_THUMB", thumb); + _logger.LogError("Wait 10 seconds before closing the program. This is to prevent a restart loop from hanging the machine."); +#pragma warning disable CA2016 // 將 'CancellationToken' 參數轉送給方法 + await Task.Delay(TimeSpan.FromSeconds(10)); +#pragma warning restore CA2016 // 將 'CancellationToken' 參數轉送給方法 } + } - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = $"{nameof(SourceGenerationContext)} is used.")] - private async Task ProcessChats(CancellationToken stoppingToken) - { - // Notice: yt-dlp在Linux會使用lock鎖定此檔案,在Windows不鎖定。 - // 實作: https://github.com/yt-dlp/yt-dlp/commit/897376719871279eef89426b1452abb89051f0dc - // Issue: https://github.com/yt-dlp/yt-dlp/issues/3124 - // 不像Windows是獨占鎖,Linux上是諮詢鎖,程式可以自行決定是否遵守鎖定。 - // FileStream「會」遵守鎖定,所以此處會在開啟檔案時報錯。 - // 詳細說明請參考這個issue,其中的討論過程非常清楚: https://github.com/dotnet/runtime/issues/34126 - // 這是.NET Core在Linux、Windows上關於鎖定設計的描述: https://github.com/dotnet/runtime/pull/55256 - // 如果要繞過這個問題,從.NET 6開始,可以加上環境變數「DOTNET_SYSTEM_IO_DISABLEFILELOCKING」讓FileStream「不」遵守鎖定。 - // (本專案已在Dockerfile加上此環境變數) - using FileStream fs = new(_liveChatFileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - using StreamReader sr = new(fs); - - sr.BaseStream.Seek(_position, SeekOrigin.Begin); - while (_position < sr.BaseStream.Length) - { - string? str = ""; - try - { - str = await sr.ReadLineAsync(stoppingToken); - _position = sr.BaseStream.Position; - if (string.IsNullOrEmpty(str)) continue; - - Chat? chat = JsonSerializer.Deserialize( - str, - options: new() - { - TypeInfoResolver = SourceGenerationContext.Default - }); - if (null == chat) continue; - - await BuildRequestAndSendToDiscord(chat, stoppingToken); - } - catch (JsonException e) - { - _logger.LogError("{error}", e.Message); - _logger.LogError("{originalString}", str); - } - catch (ArgumentException e) - { - _logger.LogError("{error}", e.Message); - _logger.LogError("{originalString}", str); - } - catch (IOException e) - { - _logger.LogError("{error}", e.Message); - break; - } - } - } + /// + /// Monitoring + /// + /// + /// + /// + private async Task Monitoring(CancellationToken stoppingToken) + { + await GetVideoInfo(stoppingToken); - /// - /// 建立Discord embed並送出至Webhook - /// - /// - /// - /// - /// 訊息格式未支援 - private async Task BuildRequestAndSendToDiscord(Chat chat, CancellationToken stoppingToken) +#if !DEBUG + if (null == Environment.GetEnvironmentVariable("SKIP_STARTUP_WAITING")) { - EmbedBuilder eb = new(); - eb.WithTitle(Environment.GetEnvironmentVariable("TITLE") ?? "") - .WithUrl($"https://youtu.be/{_id}") - .WithThumbnailUrl(Helper.GetOriginalImage(Environment.GetEnvironmentVariable("VIDEO_THUMB"))); - string author = ""; + _logger.LogInformation("Wait 1 miunute to skip old chats"); + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + _liveChatFileInfo.Refresh(); + } +#endif - var liveChatTextMessage = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatTextMessageRenderer; - var liveChatPaidMessage = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatPaidMessageRenderer; - var liveChatPaidSticker = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatPaidStickerRenderer; - var liveChatPurchaseSponsorshipsGift = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatSponsorshipsGiftPurchaseAnnouncementRenderer; + _position = _liveChatFileInfo.Length; + _logger.LogInformation("Start at position: {position}", _position); + _logger.LogInformation("Start Monitoring!"); - // ReplaceChat: Treat as a new message - // This is rare and not easy to test. - // If it behaves strangely, please open a new issue with more examples. - var replaceChat = chat.replayChatItemAction?.actions?.FirstOrDefault()?.replaceChatItemAction?.replacementItem?.liveChatTextMessageRenderer; - if (null != replaceChat) + while (!stoppingToken.IsCancellationRequested) + { + _liveChatFileInfo.Refresh(); + if (_liveChatFileInfo.Length > _position) { - liveChatTextMessage = replaceChat; + await ProcessChats(stoppingToken); } - - // Normal Message - if (null != liveChatTextMessage) + else if (_liveChatDownloadService.downloadProcess.IsCompleted) { - List runs = liveChatTextMessage.message?.runs ?? new List(); - author = liveChatTextMessage.authorName?.simpleText ?? ""; - string authorPhoto = Helper.GetOriginalImage(liveChatTextMessage.authorPhoto?.thumbnails?.LastOrDefault()?.url); - - eb.WithDescription(string.Join("", runs.Select(p => p.text ?? (p.emoji?.searchTerms?.FirstOrDefault())))) - .WithAuthor(new EmbedAuthorBuilder().WithName(author) - .WithUrl($"https://www.youtube.com/channel/{liveChatTextMessage.authorExternalChannelId}") - .WithIconUrl(authorPhoto)); - - // Timestamp - long timeStamp = long.TryParse(liveChatTextMessage.timestampUsec, out long l) ? l / 1000 : 0; - EmbedFooterBuilder ft = new(); - string authorBadgeUrl = Helper.GetOriginalImage(liveChatTextMessage.authorBadges?.FirstOrDefault()?.liveChatAuthorBadgeRenderer?.customThumbnail?.thumbnails?.LastOrDefault()?.url); - ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp) - .LocalDateTime - .ToString("yyyy/MM/dd HH:mm:ss")) - .WithIconUrl(authorBadgeUrl); - - // From Stream Owner - //if (liveChatTextMessage.authorBadges?[0].liveChatAuthorBadgeRenderer?.icon?.iconType == "OWNER") - if (liveChatTextMessage.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID")) - { - eb.WithColor(Color.Gold); - ft.WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/crown.png"); - } - - eb.WithFooter(ft); + _logger.LogInformation("Download process is stopped. Restart monitoring."); + return; } - else if (null != liveChatPaidMessage) - // Super Chat + else { - List runs = liveChatPaidMessage.message?.runs ?? new List(); - - author = liveChatPaidMessage.authorName?.simpleText ?? ""; - string authorPhoto = Helper.GetOriginalImage(liveChatPaidMessage.authorPhoto?.thumbnails?.LastOrDefault()?.url); - - eb.WithDescription(string.Join("", runs.Select(p => p.text ?? (p.emoji?.searchTerms?.FirstOrDefault())))) - .WithAuthor(new EmbedAuthorBuilder().WithName(author) - .WithUrl($"https://www.youtube.com/channel/{liveChatPaidMessage.authorExternalChannelId}") - .WithIconUrl(authorPhoto)); - - // Super Chat Amount - eb.WithFields(new EmbedFieldBuilder[] { new EmbedFieldBuilder().WithName("Amount").WithValue(liveChatPaidMessage.purchaseAmountText?.simpleText) }); - - // Super Chat Background Color - Color bgColor = (Color)System.Drawing.ColorTranslator.FromHtml(string.Format("#{0:X}", liveChatPaidMessage.bodyBackgroundColor)); - eb.WithColor(bgColor); - - // Timestamp - long timeStamp = long.TryParse(liveChatPaidMessage.timestampUsec, out long l) ? l / 1000 : 0; - EmbedFooterBuilder ft = new(); - ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp) - .LocalDateTime - .ToString("yyyy/MM/dd HH:mm:ss")) - .WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/wallet.png"); - - // From Stream Owner - if (liveChatPaidMessage.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID")) - { - eb.WithColor(Color.Gold); - ft.WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/crown.png"); - } - - eb.WithFooter(ft); + _position = _liveChatFileInfo.Length; + _logger.LogTrace("No new chat. Wait 10 seconds."); + // 每10秒檢查一次json檔 + await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); } - else if (null != liveChatPaidSticker) - // Super Chat Sticker - { - author = liveChatPaidSticker.authorName?.simpleText ?? ""; - string authorPhoto = Helper.GetOriginalImage(liveChatPaidSticker.authorPhoto?.thumbnails?.LastOrDefault()?.url); - - eb.WithDescription("") - .WithAuthor(new EmbedAuthorBuilder().WithName(author) - .WithUrl($"https://www.youtube.com/channel/{liveChatPaidSticker.authorExternalChannelId}") - .WithIconUrl(authorPhoto)); - - // Super Chat Amount - eb.WithFields(new EmbedFieldBuilder[] { new EmbedFieldBuilder().WithName("Amount").WithValue(liveChatPaidSticker.purchaseAmountText?.simpleText) }); - - // Super Chat Background Color - Color bgColor = (Color)System.Drawing.ColorTranslator.FromHtml(string.Format("#{0:X}", liveChatPaidSticker.backgroundColor)); - eb.WithColor(bgColor); + } + } - // Super Chat Sticker Picture - string stickerThumbUrl = Helper.GetOriginalImage("https:" + liveChatPaidSticker.sticker?.thumbnails?.LastOrDefault()?.url); - eb.WithThumbnailUrl(stickerThumbUrl); + /// + /// GetVideoInfo + /// + /// + /// + /// + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = $"{nameof(SourceGenerationContext)} is used.")] + private async Task GetVideoInfo(CancellationToken stoppingToken) + { + FileInfo videoInfo = new($"{_id}.info.json"); + if (!videoInfo.Exists) + { + // Chat json file 在 VideoInfo json file之後被產生,理論上這段不會進來 + throw new FileNotFoundException(null, videoInfo.FullName); + } - // Timestamp - long timeStamp = long.TryParse(liveChatPaidSticker.timestampUsec, out long l) ? l / 1000 : 0; - EmbedFooterBuilder ft = new(); - ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp) - .LocalDateTime - .ToString("yyyy/MM/dd HH:mm:ss")) - .WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/wallet.png"); + Info? info = JsonSerializer.Deserialize(json: await new StreamReader(videoInfo.OpenRead()).ReadToEndAsync(stoppingToken), + jsonTypeInfo: SourceGenerationContext.Default.info); + string? Title = info?.title; + string? ChannelId = info?.channel_id; + string? thumb = info?.thumbnail; - // From Stream Owner - if (liveChatPaidSticker.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID")) - { - eb.WithColor(Color.Gold); - ft.WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/crown.png"); - } + Environment.SetEnvironmentVariable("TITLE", Title); + Environment.SetEnvironmentVariable("CHANNEL_ID", ChannelId); + Environment.SetEnvironmentVariable("VIDEO_THUMB", thumb); + } - eb.WithFooter(ft); - } - else if (null != liveChatPurchaseSponsorshipsGift && null != liveChatPurchaseSponsorshipsGift?.header?.liveChatSponsorshipsHeaderRenderer) - // Purchase Sponsorships Gift + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = $"{nameof(SourceGenerationContext)} is used.")] + private async Task ProcessChats(CancellationToken stoppingToken) + { + // Notice: yt-dlp在Linux會使用lock鎖定此檔案,在Windows不鎖定。 + // 實作: https://github.com/yt-dlp/yt-dlp/commit/897376719871279eef89426b1452abb89051f0dc + // Issue: https://github.com/yt-dlp/yt-dlp/issues/3124 + // 不像Windows是獨占鎖,Linux上是諮詢鎖,程式可以自行決定是否遵守鎖定。 + // FileStream「會」遵守鎖定,所以此處會在開啟檔案時報錯。 + // 詳細說明請參考這個issue,其中的討論過程非常清楚: https://github.com/dotnet/runtime/issues/34126 + // 這是.NET Core在Linux、Windows上關於鎖定設計的描述: https://github.com/dotnet/runtime/pull/55256 + // 如果要繞過這個問題,從.NET 6開始,可以加上環境變數「DOTNET_SYSTEM_IO_DISABLEFILELOCKING」讓FileStream「不」遵守鎖定。 + // (本專案已在Dockerfile加上此環境變數) + using FileStream fs = new(_liveChatFileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using StreamReader sr = new(fs); + + sr.BaseStream.Seek(_position, SeekOrigin.Begin); + while (_position < sr.BaseStream.Length) + { + string? str = ""; + try { - LiveChatSponsorshipsHeaderRenderer header = liveChatPurchaseSponsorshipsGift.header.liveChatSponsorshipsHeaderRenderer; - author = header.authorName?.simpleText ?? ""; - string authorPhoto = Helper.GetOriginalImage(header.authorPhoto?.thumbnails?.LastOrDefault()?.url); + str = await sr.ReadLineAsync(stoppingToken); + _position = sr.BaseStream.Position; + if (string.IsNullOrEmpty(str)) continue; - eb.WithDescription("") - .WithAuthor(new EmbedAuthorBuilder().WithName(author) - .WithUrl($"https://www.youtube.com/channel/{liveChatPurchaseSponsorshipsGift?.authorExternalChannelId}") - .WithIconUrl(authorPhoto)); + Chat? chat = JsonSerializer.Deserialize(json: str, + jsonTypeInfo: SourceGenerationContext.Default.chat); + if (null == chat) continue; - // Gift Amount - eb.WithFields(new EmbedFieldBuilder[] { new EmbedFieldBuilder().WithName("Amount").WithValue(header?.primaryText?.runs?[1].text) }); - - // Gift Background Color - Color bgColor = (Color)System.Drawing.ColorTranslator.FromHtml("#0f9d58"); - eb.WithColor(bgColor); - - // Gift Picture - string? giftThumbUrl = header?.image?.thumbnails?.LastOrDefault()?.url; - if (null != giftThumbUrl) eb.WithThumbnailUrl(giftThumbUrl); - - // Timestamp - long timeStamp = long.TryParse(liveChatPurchaseSponsorshipsGift?.timestampUsec, out long l) ? l / 1000 : 0; - EmbedFooterBuilder ft = new(); - ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp) - .LocalDateTime - .ToString("yyyy/MM/dd HH:mm:ss")) - .WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/wallet.png"); - - // From Stream Owner - if (liveChatPurchaseSponsorshipsGift?.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID")) - { - //eb.WithColor(Color.Gold); - ft.WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/crown.png"); - } - - eb.WithFooter(ft); + await _discordService.BuildRequestAndSendToDiscord(chat, stoppingToken); } - // Discrad known garbage messages. - else if ( - // Banner Pinned message. - null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addBannerToLiveChatCommand - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.removeBannerForLiveChatCommand - // Click to show less. - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.showLiveChatTooltipCommand - // Welcome to live chat! Remember to guard your privacy and abide by our community guidelines. - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatViewerEngagementMessageRenderer - // Membership messages. - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatMembershipItemRenderer - // SC Ticker messages. - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addLiveChatTickerItemAction - // Delete messages. - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.markChatItemAsDeletedAction - // Remove Chat Item. Not really sure what this is. - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.removeChatItemAction - // Live chat mode change. - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatModeChangeMessageRenderer - // Poll - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.updateLiveChatPollAction - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.closeLiveChatActionPanelAction - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.showLiveChatActionPanelAction - // Sponsorships Gift redemption - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatSponsorshipsGiftRedemptionAnnouncementRenderer - // Have no idea what this is - || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatPlaceholderItemRenderer - ) { return; } - else + catch (JsonException e) { - _logger.LogWarning("Message type not supported, skip sending to discord."); - throw new ArgumentException("Message type not supported", nameof(chat)); + _logger.LogError("{error}", e.Message); + _logger.LogError("{originalString}", str); } - - if (stoppingToken.IsCancellationRequested) return; - - _logger.LogDebug("Sending Request to Discord: {author}: {message}", author, eb.Description); - - try + catch (ArgumentException e) { - await SendMessage(); + _logger.LogError("{error}", e.Message); + _logger.LogError("{originalString}", str); } - catch (TimeoutException) { } - // System.Net.Http.HttpRequestException: Resource temporarily unavailable (discord.com:443) - catch (HttpRequestException) + catch (IOException e) { - // Retry once after 5 sec - await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); - await SendMessage(); + _logger.LogError("{error}", e.Message); + break; } - - // The rate for Discord webhooks are 30 requests/minute per channel. - // Be careful when you run multiple instances in the same channel! - _logger.LogTrace("Wait 2 seconds for discord webhook rate limit"); - await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); - - Task SendMessage() - => _client.SendMessageAsync(embeds: new Embed[] { eb.Build() }) - .ContinueWith(async p => - { - ulong messageId = await p; - _logger.LogDebug("Message sent to discord, message id: {messageId}", messageId); - }, stoppingToken); } } } \ No newline at end of file diff --git a/Models/Chat.cs b/Models/Chat.cs index f491086..ef7e233 100644 --- a/Models/Chat.cs +++ b/Models/Chat.cs @@ -1,654 +1,662 @@ -namespace YoutubeLiveChatToDiscord.Models; - -/* These POCOs are generated from the results by the code generator. - * https://json2csharp.com/ - */ - -#pragma warning disable IDE1006 // 命名樣式 +#pragma warning disable IDE1006 // 命名樣式 #nullable disable -public class ContextMenuButton -{ - public ButtonRenderer buttonRenderer { get; set; } -} - -public class Icon -{ - public string iconType { get; set; } -} - -public class LiveChatBannerHeaderRenderer -{ - public Icon icon { get; set; } - public Text text { get; set; } - public ContextMenuButton contextMenuButton { get; set; } -} - -public class Header -{ - public LiveChatBannerHeaderRenderer liveChatBannerHeaderRenderer { get; set; } - public LiveChatSponsorshipsHeaderRenderer liveChatSponsorshipsHeaderRenderer { get; set; } - public PollHeaderRenderer pollHeaderRenderer { get; set; } - -} - -public class AccessibilityData -{ - public string label { get; set; } - public AccessibilityData accessibilityData { get; set; } -} - -public class Accessibility -{ - public string label { get; set; } - public AccessibilityData accessibilityData { get; set; } -} - -public class Image -{ - public List thumbnails { get; set; } - public Accessibility accessibility { get; set; } -} - -public class Emoji -{ - public string emojiId { get; set; } - public List shortcuts { get; set; } - public List searchTerms { get; set; } - public Image image { get; set; } - public bool isCustomEmoji { get; set; } - public bool supportsSkinTone { get; set; } - public List variantIds { get; set; } -} - -public class Run -{ - public Emoji emoji { get; set; } - public string text { get; set; } - public bool bold { get; set; } - public bool italics { get; set; } -} - -public class Message -{ - public List runs { get; set; } -} - -public class HeaderSubtext -{ - public List runs { get; set; } -} - -public class AuthorName -{ - public string simpleText { get; set; } -} - -public class AuthorPhoto -{ - public List thumbnails { get; set; } - public Accessibility accessibility { get; set; } -} - -public class WebCommandMetadata -{ - public bool ignoreNavigation { get; set; } - public string url { get; set; } - public string webPageType { get; set; } - public int rootVe { get; set; } - public string apiUrl { get; set; } - public bool sendPost { get; set; } -} - -public class CommandMetadata -{ - public WebCommandMetadata webCommandMetadata { get; set; } -} - -public class LiveChatItemContextMenuEndpoint -{ - public string @params { get; set; } -} - -public class ContextMenuEndpoint -{ - public string clickTrackingParams { get; set; } - public CommandMetadata commandMetadata { get; set; } - public LiveChatItemContextMenuEndpoint liveChatItemContextMenuEndpoint { get; set; } -} - -public class CustomThumbnail -{ - public List thumbnails { get; set; } -} - -public class LiveChatAuthorBadgeRenderer -{ - public CustomThumbnail customThumbnail { get; set; } - public string tooltip { get; set; } - public Accessibility accessibility { get; set; } - public Icon icon { get; set; } -} - -public class AuthorBadge -{ - public LiveChatAuthorBadgeRenderer liveChatAuthorBadgeRenderer { get; set; } -} - -public class ContextMenuAccessibility -{ - public AccessibilityData accessibilityData { get; set; } -} - -public class LiveChatTextMessageRenderer -{ - public Message message { get; set; } - public AuthorName authorName { get; set; } - public AuthorPhoto authorPhoto { get; set; } - public ContextMenuEndpoint contextMenuEndpoint { get; set; } - public string id { get; set; } - public string timestampUsec { get; set; } - public List authorBadges { get; set; } - public string authorExternalChannelId { get; set; } - public ContextMenuAccessibility contextMenuAccessibility { get; set; } -} - -public class PurchaseAmountText -{ - public string simpleText { get; set; } -} - -public class LiveChatPaidMessageRenderer -{ - public string id { get; set; } - public string timestampUsec { get; set; } - public AuthorName authorName { get; set; } - public AuthorPhoto authorPhoto { get; set; } - public PurchaseAmountText purchaseAmountText { get; set; } - public Message message { get; set; } - public object headerBackgroundColor { get; set; } - public object headerTextColor { get; set; } - public object bodyBackgroundColor { get; set; } - public object bodyTextColor { get; set; } - public string authorExternalChannelId { get; set; } - public object authorNameTextColor { get; set; } - public ContextMenuEndpoint contextMenuEndpoint { get; set; } - public object timestampColor { get; set; } - public ContextMenuAccessibility contextMenuAccessibility { get; set; } - public string trackingParams { get; set; } -} - -public class Text -{ - public string simpleText { get; set; } - public List runs { get; set; } -} - -public class UrlEndpoint -{ - public string url { get; set; } - public string target { get; set; } - public bool nofollow { get; set; } -} - -public class NavigationEndpoint -{ - public string clickTrackingParams { get; set; } - public CommandMetadata commandMetadata { get; set; } - public UrlEndpoint urlEndpoint { get; set; } -} - -public class Command -{ - public string clickTrackingParams { get; set; } - public CommandMetadata commandMetadata { get; set; } - public LiveChatItemContextMenuEndpoint liveChatItemContextMenuEndpoint { get; set; } -} - -public class ButtonRenderer -{ - public string style { get; set; } - public string size { get; set; } - public bool isDisabled { get; set; } - public Text text { get; set; } - public NavigationEndpoint navigationEndpoint { get; set; } - public string trackingParams { get; set; } - public AccessibilityData accessibilityData { get; set; } - public Icon icon { get; set; } - public Command command { get; set; } - public Accessibility accessibility { get; set; } - public string targetId { get; set; } -} - -public class ActionButton -{ - public ButtonRenderer buttonRenderer { get; set; } -} - -public class LiveChatViewerEngagementMessageRenderer -{ - public string id { get; set; } - public string timestampUsec { get; set; } - public Icon icon { get; set; } - public Message message { get; set; } - public ActionButton actionButton { get; set; } -} - -public class Sticker -{ - public List thumbnails { get; set; } - public Accessibility accessibility { get; set; } -} - -public class LiveChatPaidStickerRenderer -{ - public string id { get; set; } - public ContextMenuEndpoint contextMenuEndpoint { get; set; } - public ContextMenuAccessibility contextMenuAccessibility { get; set; } - public string timestampUsec { get; set; } - public AuthorPhoto authorPhoto { get; set; } - public AuthorName authorName { get; set; } - public string authorExternalChannelId { get; set; } - public Sticker sticker { get; set; } - public long moneyChipBackgroundColor { get; set; } - public long moneyChipTextColor { get; set; } - public PurchaseAmountText purchaseAmountText { get; set; } - public int stickerDisplayWidth { get; set; } - public int stickerDisplayHeight { get; set; } - public long backgroundColor { get; set; } - public long authorNameTextColor { get; set; } - public string trackingParams { get; set; } -} - -public class LiveChatMembershipItemRenderer -{ - public string id { get; set; } - public string timestampUsec { get; set; } - public string authorExternalChannelId { get; set; } - public HeaderSubtext headerSubtext { get; set; } - public AuthorName authorName { get; set; } - public AuthorPhoto authorPhoto { get; set; } - public List authorBadges { get; set; } - public ContextMenuEndpoint contextMenuEndpoint { get; set; } - public ContextMenuAccessibility contextMenuAccessibility { get; set; } - public string trackingParams { get; set; } -} - -public class Item -{ - public LiveChatTextMessageRenderer liveChatTextMessageRenderer { get; set; } - public LiveChatPaidMessageRenderer liveChatPaidMessageRenderer { get; set; } - public LiveChatViewerEngagementMessageRenderer liveChatViewerEngagementMessageRenderer { get; set; } - public LiveChatPaidStickerRenderer liveChatPaidStickerRenderer { get; set; } - public LiveChatTickerPaidMessageItemRenderer liveChatTickerPaidMessageItemRenderer { get; set; } - public LiveChatMembershipItemRenderer liveChatMembershipItemRenderer { get; set; } - public LiveChatModeChangeMessageRenderer liveChatModeChangeMessageRenderer { get; set; } - public LiveChatSponsorshipsGiftPurchaseAnnouncementRenderer liveChatSponsorshipsGiftPurchaseAnnouncementRenderer { get; set; } - public LiveChatSponsorshipsGiftRedemptionAnnouncementRenderer liveChatSponsorshipsGiftRedemptionAnnouncementRenderer { get; set; } - public LiveChatPlaceholderItemRenderer liveChatPlaceholderItemRenderer { get; set; } -} - -public class AddChatItemAction -{ - public Item item { get; set; } - public string clientId { get; set; } -} - -public class DeletedStateMessage -{ - public List runs { get; set; } -} - -public class MarkChatItemAsDeletedAction -{ - public DeletedStateMessage deletedStateMessage { get; set; } - public string targetItemId { get; set; } -} - -public class Amount -{ - public string simpleText { get; set; } -} - -public class Renderer -{ - public LiveChatPaidMessageRenderer liveChatPaidMessageRenderer { get; set; } -} - -public class ShowLiveChatItemEndpoint -{ - public Renderer renderer { get; set; } - public string trackingParams { get; set; } -} - -public class ShowItemEndpoint -{ - public string clickTrackingParams { get; set; } - public CommandMetadata commandMetadata { get; set; } - public ShowLiveChatItemEndpoint showLiveChatItemEndpoint { get; set; } -} - -public class LiveChatTickerPaidMessageItemRenderer -{ - public string id { get; set; } - public Amount amount { get; set; } - public long amountTextColor { get; set; } - public long startBackgroundColor { get; set; } - public long endBackgroundColor { get; set; } - public AuthorPhoto authorPhoto { get; set; } - public int durationSec { get; set; } - public ShowItemEndpoint showItemEndpoint { get; set; } - public string authorExternalChannelId { get; set; } - public int fullDurationSec { get; set; } - public string trackingParams { get; set; } -} - -public class AddLiveChatTickerItemAction -{ - public Item item { get; set; } - public string durationSec { get; set; } -} - -public class UiActions -{ - public bool hideEnclosingContainer { get; set; } -} - -public class FeedbackEndpoint -{ - public string feedbackToken { get; set; } - public UiActions uiActions { get; set; } -} - -public class ImpressionEndpoint -{ - public string clickTrackingParams { get; set; } - public CommandMetadata commandMetadata { get; set; } - public FeedbackEndpoint feedbackEndpoint { get; set; } -} - -public class AcceptCommand -{ - public string clickTrackingParams { get; set; } - public CommandMetadata commandMetadata { get; set; } - public FeedbackEndpoint feedbackEndpoint { get; set; } -} - -public class DismissCommand -{ - public string clickTrackingParams { get; set; } - public CommandMetadata commandMetadata { get; set; } - public FeedbackEndpoint feedbackEndpoint { get; set; } -} - -public class PromoConfig -{ - public string promoId { get; set; } - public List impressionEndpoints { get; set; } - public AcceptCommand acceptCommand { get; set; } - public DismissCommand dismissCommand { get; set; } -} - -public class DetailsText -{ - public List runs { get; set; } -} - -public class SuggestedPosition -{ - public string type { get; set; } -} - -public class DismissStrategy -{ - public string type { get; set; } -} - -public class TooltipRenderer -{ - public PromoConfig promoConfig { get; set; } - public string targetId { get; set; } - public DetailsText detailsText { get; set; } - public SuggestedPosition suggestedPosition { get; set; } - public DismissStrategy dismissStrategy { get; set; } - public string dwellTimeMs { get; set; } - public string trackingParams { get; set; } -} - -public class Tooltip -{ - public TooltipRenderer tooltipRenderer { get; set; } -} - -public class ShowLiveChatTooltipCommand -{ - public Tooltip tooltip { get; set; } -} - -public class Contents -{ - public LiveChatTextMessageRenderer liveChatTextMessageRenderer { get; set; } - public PollRenderer pollRenderer { get; set; } -} - -public class LiveChatBannerRenderer -{ - public Header header { get; set; } - public Contents contents { get; set; } - public string actionId { get; set; } - public bool viewerIsCreator { get; set; } - public string targetId { get; set; } - public bool isStackable { get; set; } - public string backgroundType { get; set; } -} - -public class BannerRenderer -{ - public LiveChatBannerRenderer liveChatBannerRenderer { get; set; } -} - -public class AddBannerToLiveChatCommand -{ - public BannerRenderer bannerRenderer { get; set; } -} - -public class RemoveBannerForLiveChatCommand -{ - public string targetActionId { get; set; } -} - -public class UpdateLiveChatPollAction -{ - public PollToUpdate pollToUpdate { get; set; } -} - -public class CloseLiveChatActionPanelAction -{ - public string targetPanelId { get; set; } - public bool skipOnDismissCommand { get; set; } -} - -public class Action -{ - public string clickTrackingParams { get; set; } - public AddChatItemAction addChatItemAction { get; set; } - public MarkChatItemAsDeletedAction markChatItemAsDeletedAction { get; set; } - public AddLiveChatTickerItemAction addLiveChatTickerItemAction { get; set; } - public ShowLiveChatTooltipCommand showLiveChatTooltipCommand { get; set; } - public AddBannerToLiveChatCommand addBannerToLiveChatCommand { get; set; } - public ReplaceChatItemAction replaceChatItemAction { get; set; } - public RemoveChatItemAction removeChatItemAction { get; set; } - public RemoveBannerForLiveChatCommand removeBannerForLiveChatCommand { get; set; } - public UpdateLiveChatPollAction updateLiveChatPollAction { get; set; } - public CloseLiveChatActionPanelAction closeLiveChatActionPanelAction { get; set; } - public ShowLiveChatActionPanelAction showLiveChatActionPanelAction { get; set; } -} - -public class RemoveChatItemAction -{ - public string targetItemId { get; set; } -} - -public class ReplayChatItemAction -{ - public List actions { get; set; } -} +namespace YoutubeLiveChatToDiscord.Models; public class Chat { - public ReplayChatItemAction replayChatItemAction { get; set; } - public string videoOffsetTimeMsec { get; set; } - public bool isLive { get; set; } + public class ContextMenuButton + { + public ButtonRenderer buttonRenderer { get; set; } + } + + public class Icon + { + public string iconType { get; set; } + } + + public class LiveChatBannerHeaderRenderer + { + public Icon icon { get; set; } + public Text text { get; set; } + public ContextMenuButton contextMenuButton { get; set; } + } + + public class Header + { + public LiveChatBannerHeaderRenderer liveChatBannerHeaderRenderer { get; set; } + public LiveChatSponsorshipsHeaderRenderer liveChatSponsorshipsHeaderRenderer { get; set; } + public PollHeaderRenderer pollHeaderRenderer { get; set; } + } + + public class AccessibilityData + { + public string label { get; set; } + public AccessibilityData accessibilityData { get; set; } + } + + public class Accessibility + { + public string label { get; set; } + public AccessibilityData accessibilityData { get; set; } + } + + public class Image + { + public List thumbnails { get; set; } + public Accessibility accessibility { get; set; } + } + + public class Emoji + { + public string emojiId { get; set; } + public List shortcuts { get; set; } + public List searchTerms { get; set; } + public Image image { get; set; } + public bool isCustomEmoji { get; set; } + public bool supportsSkinTone { get; set; } + public List variantIds { get; set; } + } + + public class Run + { + public Emoji emoji { get; set; } + public string text { get; set; } + public bool bold { get; set; } + public bool italics { get; set; } + } + + public class Message + { + public List runs { get; set; } + } + + public class HeaderSubtext + { + public List runs { get; set; } + } + + public class AuthorName + { + public string simpleText { get; set; } + } + + public class AuthorPhoto + { + public List thumbnails { get; set; } + public Accessibility accessibility { get; set; } + } + + public class WebCommandMetadata + { + public bool ignoreNavigation { get; set; } + public string url { get; set; } + public string webPageType { get; set; } + public int rootVe { get; set; } + public string apiUrl { get; set; } + public bool sendPost { get; set; } + } + + public class CommandMetadata + { + public WebCommandMetadata webCommandMetadata { get; set; } + } + + public class LiveChatItemContextMenuEndpoint + { + public string @params { get; set; } + } + + public class ContextMenuEndpoint + { + public string clickTrackingParams { get; set; } + public CommandMetadata commandMetadata { get; set; } + public LiveChatItemContextMenuEndpoint liveChatItemContextMenuEndpoint { get; set; } + } + + public class CustomThumbnail + { + public List thumbnails { get; set; } + } + + public class LiveChatAuthorBadgeRenderer + { + public CustomThumbnail customThumbnail { get; set; } + public string tooltip { get; set; } + public Accessibility accessibility { get; set; } + public Icon icon { get; set; } + } + + public class AuthorBadge + { + public LiveChatAuthorBadgeRenderer liveChatAuthorBadgeRenderer { get; set; } + } + + public class ContextMenuAccessibility + { + public AccessibilityData accessibilityData { get; set; } + } + + public class LiveChatTextMessageRenderer + { + public Message message { get; set; } + public AuthorName authorName { get; set; } + public AuthorPhoto authorPhoto { get; set; } + public ContextMenuEndpoint contextMenuEndpoint { get; set; } + public string id { get; set; } + public string timestampUsec { get; set; } + public List authorBadges { get; set; } + public string authorExternalChannelId { get; set; } + public ContextMenuAccessibility contextMenuAccessibility { get; set; } + } + + public class PurchaseAmountText + { + public string simpleText { get; set; } + } + + public class LiveChatPaidMessageRenderer + { + public string id { get; set; } + public string timestampUsec { get; set; } + public AuthorName authorName { get; set; } + public AuthorPhoto authorPhoto { get; set; } + public PurchaseAmountText purchaseAmountText { get; set; } + public Message message { get; set; } + public object headerBackgroundColor { get; set; } + public object headerTextColor { get; set; } + public object bodyBackgroundColor { get; set; } + public object bodyTextColor { get; set; } + public string authorExternalChannelId { get; set; } + public object authorNameTextColor { get; set; } + public ContextMenuEndpoint contextMenuEndpoint { get; set; } + public object timestampColor { get; set; } + public ContextMenuAccessibility contextMenuAccessibility { get; set; } + public string trackingParams { get; set; } + } + + public class Text + { + public string simpleText { get; set; } + public List runs { get; set; } + } + + public class UrlEndpoint + { + public string url { get; set; } + public string target { get; set; } + public bool nofollow { get; set; } + } + + public class NavigationEndpoint + { + public string clickTrackingParams { get; set; } + public CommandMetadata commandMetadata { get; set; } + public UrlEndpoint urlEndpoint { get; set; } + } + + public class Command + { + public string clickTrackingParams { get; set; } + public CommandMetadata commandMetadata { get; set; } + public LiveChatItemContextMenuEndpoint liveChatItemContextMenuEndpoint { get; set; } + } + + public class ButtonRenderer + { + public string style { get; set; } + public string size { get; set; } + public bool isDisabled { get; set; } + public Text text { get; set; } + public NavigationEndpoint navigationEndpoint { get; set; } + public string trackingParams { get; set; } + public AccessibilityData accessibilityData { get; set; } + public Icon icon { get; set; } + public Command command { get; set; } + public Accessibility accessibility { get; set; } + public string targetId { get; set; } + } + + public class ActionButton + { + public ButtonRenderer buttonRenderer { get; set; } + } + + public class LiveChatViewerEngagementMessageRenderer + { + public string id { get; set; } + public string timestampUsec { get; set; } + public Icon icon { get; set; } + public Message message { get; set; } + public ActionButton actionButton { get; set; } + } + + public class Sticker + { + public List thumbnails { get; set; } + public Accessibility accessibility { get; set; } + } + + public class LiveChatPaidStickerRenderer + { + public string id { get; set; } + public ContextMenuEndpoint contextMenuEndpoint { get; set; } + public ContextMenuAccessibility contextMenuAccessibility { get; set; } + public string timestampUsec { get; set; } + public AuthorPhoto authorPhoto { get; set; } + public AuthorName authorName { get; set; } + public string authorExternalChannelId { get; set; } + public Sticker sticker { get; set; } + public long moneyChipBackgroundColor { get; set; } + public long moneyChipTextColor { get; set; } + public PurchaseAmountText purchaseAmountText { get; set; } + public int stickerDisplayWidth { get; set; } + public int stickerDisplayHeight { get; set; } + public long backgroundColor { get; set; } + public long authorNameTextColor { get; set; } + public string trackingParams { get; set; } + } + + public class LiveChatMembershipItemRenderer + { + public string id { get; set; } + public string timestampUsec { get; set; } + public string authorExternalChannelId { get; set; } + public HeaderSubtext headerSubtext { get; set; } + public AuthorName authorName { get; set; } + public AuthorPhoto authorPhoto { get; set; } + public List authorBadges { get; set; } + public ContextMenuEndpoint contextMenuEndpoint { get; set; } + public ContextMenuAccessibility contextMenuAccessibility { get; set; } + public string trackingParams { get; set; } + } + + public class Item + { + public LiveChatTextMessageRenderer liveChatTextMessageRenderer { get; set; } + public LiveChatPaidMessageRenderer liveChatPaidMessageRenderer { get; set; } + public LiveChatViewerEngagementMessageRenderer liveChatViewerEngagementMessageRenderer { get; set; } + public LiveChatPaidStickerRenderer liveChatPaidStickerRenderer { get; set; } + public LiveChatTickerPaidMessageItemRenderer liveChatTickerPaidMessageItemRenderer { get; set; } + public LiveChatMembershipItemRenderer liveChatMembershipItemRenderer { get; set; } + public LiveChatModeChangeMessageRenderer liveChatModeChangeMessageRenderer { get; set; } + public LiveChatSponsorshipsGiftPurchaseAnnouncementRenderer liveChatSponsorshipsGiftPurchaseAnnouncementRenderer { get; set; } + public LiveChatSponsorshipsGiftRedemptionAnnouncementRenderer liveChatSponsorshipsGiftRedemptionAnnouncementRenderer { get; set; } + public LiveChatPlaceholderItemRenderer liveChatPlaceholderItemRenderer { get; set; } + } + + public class AddChatItemAction + { + public Item item { get; set; } + public string clientId { get; set; } + } + + public class DeletedStateMessage + { + public List runs { get; set; } + } + + public class MarkChatItemAsDeletedAction + { + public DeletedStateMessage deletedStateMessage { get; set; } + public string targetItemId { get; set; } + } + + public class Amount + { + public string simpleText { get; set; } + } + + public class Renderer + { + public LiveChatPaidMessageRenderer liveChatPaidMessageRenderer { get; set; } + } + + public class ShowLiveChatItemEndpoint + { + public Renderer renderer { get; set; } + public string trackingParams { get; set; } + } + + public class ShowItemEndpoint + { + public string clickTrackingParams { get; set; } + public CommandMetadata commandMetadata { get; set; } + public ShowLiveChatItemEndpoint showLiveChatItemEndpoint { get; set; } + } + + public class LiveChatTickerPaidMessageItemRenderer + { + public string id { get; set; } + public Amount amount { get; set; } + public long amountTextColor { get; set; } + public long startBackgroundColor { get; set; } + public long endBackgroundColor { get; set; } + public AuthorPhoto authorPhoto { get; set; } + public int durationSec { get; set; } + public ShowItemEndpoint showItemEndpoint { get; set; } + public string authorExternalChannelId { get; set; } + public int fullDurationSec { get; set; } + public string trackingParams { get; set; } + } + + public class AddLiveChatTickerItemAction + { + public Item item { get; set; } + public string durationSec { get; set; } + } + + public class UiActions + { + public bool hideEnclosingContainer { get; set; } + } + + public class FeedbackEndpoint + { + public string feedbackToken { get; set; } + public UiActions uiActions { get; set; } + } + + public class ImpressionEndpoint + { + public string clickTrackingParams { get; set; } + public CommandMetadata commandMetadata { get; set; } + public FeedbackEndpoint feedbackEndpoint { get; set; } + } + + public class AcceptCommand + { + public string clickTrackingParams { get; set; } + public CommandMetadata commandMetadata { get; set; } + public FeedbackEndpoint feedbackEndpoint { get; set; } + } + + public class DismissCommand + { + public string clickTrackingParams { get; set; } + public CommandMetadata commandMetadata { get; set; } + public FeedbackEndpoint feedbackEndpoint { get; set; } + } + + public class PromoConfig + { + public string promoId { get; set; } + public List impressionEndpoints { get; set; } + public AcceptCommand acceptCommand { get; set; } + public DismissCommand dismissCommand { get; set; } + } + + public class DetailsText + { + public List runs { get; set; } + } + + public class SuggestedPosition + { + public string type { get; set; } + } + + public class DismissStrategy + { + public string type { get; set; } + } + + public class TooltipRenderer + { + public PromoConfig promoConfig { get; set; } + public string targetId { get; set; } + public DetailsText detailsText { get; set; } + public SuggestedPosition suggestedPosition { get; set; } + public DismissStrategy dismissStrategy { get; set; } + public string dwellTimeMs { get; set; } + public string trackingParams { get; set; } + } + + public class Tooltip + { + public TooltipRenderer tooltipRenderer { get; set; } + } + + public class ShowLiveChatTooltipCommand + { + public Tooltip tooltip { get; set; } + } + + public class Contents + { + public LiveChatTextMessageRenderer liveChatTextMessageRenderer { get; set; } + public PollRenderer pollRenderer { get; set; } + } + + public class LiveChatBannerRenderer + { + public Header header { get; set; } + public Contents contents { get; set; } + public string actionId { get; set; } + public bool viewerIsCreator { get; set; } + public string targetId { get; set; } + public bool isStackable { get; set; } + public string backgroundType { get; set; } + } + + public class BannerRenderer + { + public LiveChatBannerRenderer liveChatBannerRenderer { get; set; } + } + + public class AddBannerToLiveChatCommand + { + public BannerRenderer bannerRenderer { get; set; } + } + + public class RemoveBannerForLiveChatCommand + { + public string targetActionId { get; set; } + } + + public class UpdateLiveChatPollAction + { + public PollToUpdate pollToUpdate { get; set; } + } + + public class CloseLiveChatActionPanelAction + { + public string targetPanelId { get; set; } + public bool skipOnDismissCommand { get; set; } + } + + public class Action + { + public string clickTrackingParams { get; set; } + public AddChatItemAction addChatItemAction { get; set; } + public MarkChatItemAsDeletedAction markChatItemAsDeletedAction { get; set; } + public AddLiveChatTickerItemAction addLiveChatTickerItemAction { get; set; } + public ShowLiveChatTooltipCommand showLiveChatTooltipCommand { get; set; } + public AddBannerToLiveChatCommand addBannerToLiveChatCommand { get; set; } + public ReplaceChatItemAction replaceChatItemAction { get; set; } + public RemoveChatItemAction removeChatItemAction { get; set; } + public RemoveBannerForLiveChatCommand removeBannerForLiveChatCommand { get; set; } + public UpdateLiveChatPollAction updateLiveChatPollAction { get; set; } + public CloseLiveChatActionPanelAction closeLiveChatActionPanelAction { get; set; } + public ShowLiveChatActionPanelAction showLiveChatActionPanelAction { get; set; } + } + + public class RemoveChatItemAction + { + public string targetItemId { get; set; } + } + + public class ReplayChatItemAction + { + public List actions { get; set; } + } + +#pragma warning disable CS8981 // 類型名稱只包含小寫的 ASCII 字元。此類名稱可能保留供此語言使用。 + public class chat +#pragma warning restore CS8981 // 類型名稱只包含小寫的 ASCII 字元。此類名稱可能保留供此語言使用。 + { + public ReplayChatItemAction replayChatItemAction { get; set; } + public string videoOffsetTimeMsec { get; set; } + public bool isLive { get; set; } + } + + public class LiveChatModeChangeMessageRenderer + { + public string id { get; set; } + public string timestampUsec { get; set; } + public Icon icon { get; set; } + public Text text { get; set; } + public Subtext subtext { get; set; } + } + + public class Root + { + public ReplayChatItemAction replayChatItemAction { get; set; } + public string videoOffsetTimeMsec { get; set; } + public bool isLive { get; set; } + } + + public class Subtext + { + public List runs { get; set; } + } + + public class ReplaceChatItemAction + { + public string targetItemId { get; set; } + public ReplacementItem replacementItem { get; set; } + } + + public class ReplacementItem + { + public LiveChatTextMessageRenderer liveChatTextMessageRenderer { get; set; } + } + + public class Choice + { + public Text text { get; set; } + public bool selected { get; set; } + public double voteRatio { get; set; } + public VotePercentage votePercentage { get; set; } + public SelectServiceEndpoint selectServiceEndpoint { get; set; } + } + + public class MetadataText + { + public List runs { get; set; } + } + + public class PollHeaderRenderer + { + public PollQuestion pollQuestion { get; set; } + public Thumbnail thumbnail { get; set; } + public MetadataText metadataText { get; set; } + public string liveChatPollType { get; set; } + public ContextMenuButton contextMenuButton { get; set; } + } + + public class PollQuestion + { + public List runs { get; set; } + } + + public class PollRenderer + { + public List choices { get; set; } + public string liveChatPollId { get; set; } + public Header header { get; set; } + public string trackingParams { get; set; } + } + + public class PollToUpdate + { + public PollRenderer pollRenderer { get; set; } + } + + public class SelectServiceEndpoint + { + public string clickTrackingParams { get; set; } + public CommandMetadata commandMetadata { get; set; } + public SendLiveChatVoteEndpoint sendLiveChatVoteEndpoint { get; set; } + } + + public class SendLiveChatVoteEndpoint + { + public string @params { get; set; } + } + + public class VotePercentage + { + public string simpleText { get; set; } + } + + public class LiveChatActionPanelRenderer + { + public Contents contents { get; set; } + public string id { get; set; } + public string targetId { get; set; } + } + + public class PanelToShow + { + public LiveChatActionPanelRenderer liveChatActionPanelRenderer { get; set; } + } + + public class ShowLiveChatActionPanelAction + { + public PanelToShow panelToShow { get; set; } + } + + public class LiveChatSponsorshipsGiftPurchaseAnnouncementRenderer + { + public string id { get; set; } + public string timestampUsec { get; set; } + public string authorExternalChannelId { get; set; } + public Header header { get; set; } + } + + public class LiveChatSponsorshipsGiftRedemptionAnnouncementRenderer + { + public string id { get; set; } + public string timestampUsec { get; set; } + public string authorExternalChannelId { get; set; } + public AuthorName authorName { get; set; } + public AuthorPhoto authorPhoto { get; set; } + public Message message { get; set; } + public ContextMenuEndpoint contextMenuEndpoint { get; set; } + public ContextMenuAccessibility contextMenuAccessibility { get; set; } + public string trackingParams { get; set; } + } + + public class LiveChatSponsorshipsHeaderRenderer + { + public AuthorName authorName { get; set; } + public AuthorPhoto authorPhoto { get; set; } + public PrimaryText primaryText { get; set; } + public List authorBadges { get; set; } + public ContextMenuEndpoint contextMenuEndpoint { get; set; } + public ContextMenuAccessibility contextMenuAccessibility { get; set; } + public Image image { get; set; } + } + + public class PrimaryText + { + public List runs { get; set; } + } + + public class LiveChatPlaceholderItemRenderer + { + public string id { get; set; } + public string timestampUsec { get; set; } + } + + public class Thumbnail + { + public string url { get; set; } + public int preference { get; set; } + public string id { get; set; } + public int height { get; set; } + public int width { get; set; } + public string resolution { get; set; } + } } - -public class LiveChatModeChangeMessageRenderer -{ - public string id { get; set; } - public string timestampUsec { get; set; } - public Icon icon { get; set; } - public Text text { get; set; } - public Subtext subtext { get; set; } -} - -public class Root -{ - public ReplayChatItemAction replayChatItemAction { get; set; } - public string videoOffsetTimeMsec { get; set; } - public bool isLive { get; set; } -} - -public class Subtext -{ - public List runs { get; set; } -} - -public class ReplaceChatItemAction -{ - public string targetItemId { get; set; } - public ReplacementItem replacementItem { get; set; } -} - -public class ReplacementItem -{ - public LiveChatTextMessageRenderer liveChatTextMessageRenderer { get; set; } -} - -public class Choice -{ - public Text text { get; set; } - public bool selected { get; set; } - public double voteRatio { get; set; } - public VotePercentage votePercentage { get; set; } - public SelectServiceEndpoint selectServiceEndpoint { get; set; } -} - -public class MetadataText -{ - public List runs { get; set; } -} - -public class PollHeaderRenderer -{ - public PollQuestion pollQuestion { get; set; } - public Thumbnail thumbnail { get; set; } - public MetadataText metadataText { get; set; } - public string liveChatPollType { get; set; } - public ContextMenuButton contextMenuButton { get; set; } -} - -public class PollQuestion -{ - public List runs { get; set; } -} - -public class PollRenderer -{ - public List choices { get; set; } - public string liveChatPollId { get; set; } - public Header header { get; set; } - public string trackingParams { get; set; } -} - -public class PollToUpdate -{ - public PollRenderer pollRenderer { get; set; } -} - -public class SelectServiceEndpoint -{ - public string clickTrackingParams { get; set; } - public CommandMetadata commandMetadata { get; set; } - public SendLiveChatVoteEndpoint sendLiveChatVoteEndpoint { get; set; } -} - -public class SendLiveChatVoteEndpoint -{ - public string @params { get; set; } -} - -public class VotePercentage -{ - public string simpleText { get; set; } -} - -public class LiveChatActionPanelRenderer -{ - public Contents contents { get; set; } - public string id { get; set; } - public string targetId { get; set; } -} - -public class PanelToShow -{ - public LiveChatActionPanelRenderer liveChatActionPanelRenderer { get; set; } -} - -public class ShowLiveChatActionPanelAction -{ - public PanelToShow panelToShow { get; set; } -} - -public class LiveChatSponsorshipsGiftPurchaseAnnouncementRenderer -{ - public string id { get; set; } - public string timestampUsec { get; set; } - public string authorExternalChannelId { get; set; } - public Header header { get; set; } -} - -public class LiveChatSponsorshipsGiftRedemptionAnnouncementRenderer -{ - public string id { get; set; } - public string timestampUsec { get; set; } - public string authorExternalChannelId { get; set; } - public AuthorName authorName { get; set; } - public AuthorPhoto authorPhoto { get; set; } - public Message message { get; set; } - public ContextMenuEndpoint contextMenuEndpoint { get; set; } - public ContextMenuAccessibility contextMenuAccessibility { get; set; } - public string trackingParams { get; set; } -} - -public class LiveChatSponsorshipsHeaderRenderer -{ - public AuthorName authorName { get; set; } - public AuthorPhoto authorPhoto { get; set; } - public PrimaryText primaryText { get; set; } - public List authorBadges { get; set; } - public ContextMenuEndpoint contextMenuEndpoint { get; set; } - public ContextMenuAccessibility contextMenuAccessibility { get; set; } - public Image image { get; set; } -} - -public class PrimaryText -{ - public List runs { get; set; } -} - -public class LiveChatPlaceholderItemRenderer -{ - public string id { get; set; } - public string timestampUsec { get; set; } -} - -#pragma warning restore IDE1006 // 命名樣式 diff --git a/Models/Info.cs b/Models/Info.cs index 5251f1e..31a4e7f 100644 --- a/Models/Info.cs +++ b/Models/Info.cs @@ -1,1040 +1,1037 @@ using System.Text.Json.Serialization; - -namespace YoutubeLiveChatToDiscord.Models; - -/* These POCOs are generated from the results by the code generator. - * https://json2csharp.com/ - */ - -// Info myDeserializedClass = JsonConvert.DeserializeObject(myJsonResponse); - #pragma warning disable IDE1006 // 命名樣式 -public class HttpHeaders -{ - [JsonPropertyName("User-Agent")] - public string? UserAgent { get; set; } - public string? Accept { get; set; } - - [JsonPropertyName("Accept-Encoding")] - public string? AcceptEncoding { get; set; } - - [JsonPropertyName("Accept-Language")] - public string? AcceptLanguage { get; set; } - - [JsonPropertyName("Sec-Fetch-Mode")] - public string? SecFetchMode { get; set; } -} - -public class Fragment -{ - public string? path { get; set; } - public double duration { get; set; } -} - -public class DownloaderOptions -{ - public int http_chunk_size { get; set; } -} - -public class Format -{ - public string? format_id { get; set; } - public string? url { get; set; } - public string? manifest_url { get; set; } - public double tbr { get; set; } - public string? ext { get; set; } - public double fps { get; set; } - public string? protocol { get; set; } - public int quality { get; set; } - public int width { get; set; } - public int height { get; set; } - public string? vcodec { get; set; } - public string? acodec { get; set; } - public string? dynamic_range { get; set; } - public string? video_ext { get; set; } - public string? audio_ext { get; set; } - public double vbr { get; set; } - public double abr { get; set; } - public string? format { get; set; } - public string? resolution { get; set; } - public HttpHeaders? http_headers { get; set; } - public string? format_note { get; set; } - public List? fragments { get; set; } - public int? asr { get; set; } - public long? filesize { get; set; } - public int? source_preference { get; set; } - public string? language { get; set; } - public int? language_preference { get; set; } - public DownloaderOptions? downloader_options { get; set; } - public string? container { get; set; } - public double? filesize_approx { get; set; } -} - -public class Thumbnail -{ - public string? url { get; set; } - public int preference { get; set; } - public string? id { get; set; } - public int? height { get; set; } - public int? width { get; set; } - public string? resolution { get; set; } -} - -public class LiveChat -{ - public string? url { get; set; } - public string? video_id { get; set; } - public string? ext { get; set; } - public string? protocol { get; set; } -} - -public class Subtitles -{ - public List? live_chat { get; set; } -} - -public class Af -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Sq -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Am -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ar -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Hy -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Az -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Bn -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Eu -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Be -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class B -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Bg -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class My -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ca -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ceb -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class ZhHan -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class ZhHant -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Co -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Hr -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class C -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Da -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Nl -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class En -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Eo -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Et -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Fil -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Fi -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Fr -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Gl -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ka -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class De -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class El -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Gu -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ht -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ha -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Haw -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Iw -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Hi -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Hmn -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Hu -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} -public class Is -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ig -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Id -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ga -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class It -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ja -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Jv -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Kn -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Kk -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Km -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Rw -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ko -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ku -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ky -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Lo -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class La -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Lv -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Lt -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Lb -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Mk -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Mg -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class M -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ml -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Mt -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Mi -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Mr -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Mn -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ne -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class No -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ny -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Or -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class P -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Fa -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Pl -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Pt -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Pa -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ro -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ru -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Sm -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Gd -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Sr -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Sn -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Sd -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Si -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Sk -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Sl -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class So -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class St -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class E -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Su -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Sw -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Sv -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Tg -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ta -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Tt -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Te -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Th -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Tr -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Tk -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Uk -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ur -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Ug -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Uz -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Vi -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Cy -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Fy -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Xh -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Yi -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Yo -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class Zu -{ - public string? ext { get; set; } - public string? url { get; set; } - public string? name { get; set; } -} - -public class AutomaticCaptions -{ - public List? af { get; set; } - public List? sq { get; set; } - public List? am { get; set; } - public List? ar { get; set; } - public List? hy { get; set; } - public List? az { get; set; } - public List? bn { get; set; } - public List? eu { get; set; } - public List? be { get; set; } - public List? bs { get; set; } - public List? bg { get; set; } - public List? my { get; set; } - public List? ca { get; set; } - public List? ceb { get; set; } - - [JsonPropertyName("zh-Hans")] - public List? ZhHans { get; set; } - - [JsonPropertyName("zh-Hant")] - public List? ZhHant { get; set; } - public List? co { get; set; } - public List
? hr { get; set; } - public List? cs { get; set; } - public List? da { get; set; } - public List? nl { get; set; } - public List? en { get; set; } - public List? eo { get; set; } - public List? et { get; set; } - public List? fil { get; set; } - public List? fi { get; set; } - public List? fr { get; set; } - public List? gl { get; set; } - public List? ka { get; set; } - public List? de { get; set; } - public List? el { get; set; } - public List? gu { get; set; } - public List? ht { get; set; } - public List? ha { get; set; } - public List? haw { get; set; } - public List? iw { get; set; } - public List? hi { get; set; } - public List? hmn { get; set; } - public List? hu { get; set; } - public List? @is { get; set; } - public List? ig { get; set; } - public List? id { get; set; } - public List? ga { get; set; } - public List? it { get; set; } - public List? ja { get; set; } - public List? jv { get; set; } - public List? kn { get; set; } - public List? kk { get; set; } - public List? km { get; set; } - public List? rw { get; set; } - public List? ko { get; set; } - public List? ku { get; set; } - public List? ky { get; set; } - public List? lo { get; set; } - public List? la { get; set; } - public List? lv { get; set; } - public List? lt { get; set; } - public List? lb { get; set; } - public List? mk { get; set; } - public List? mg { get; set; } - public List? ms { get; set; } - public List? ml { get; set; } - public List? mt { get; set; } - public List? mi { get; set; } - public List? mr { get; set; } - public List? mn { get; set; } - public List? ne { get; set; } - public List? no { get; set; } - public List? ny { get; set; } - public List? or { get; set; } - public List

? ps { get; set; } - public List? fa { get; set; } - public List? pl { get; set; } - public List? pt { get; set; } - public List? pa { get; set; } - public List? ro { get; set; } - public List? ru { get; set; } - public List? sm { get; set; } - public List? gd { get; set; } - public List? sr { get; set; } - public List? sn { get; set; } - public List? sd { get; set; } - public List? si { get; set; } - public List? sk { get; set; } - public List? sl { get; set; } - public List? so { get; set; } - public List? st { get; set; } - public List? es { get; set; } - public List? su { get; set; } - public List? sw { get; set; } - public List? sv { get; set; } - public List? tg { get; set; } - public List? ta { get; set; } - public List? tt { get; set; } - public List? te { get; set; } - public List? th { get; set; } - public List? tr { get; set; } - public List? tk { get; set; } - public List? uk { get; set; } - public List? ur { get; set; } - public List? ug { get; set; } - public List? uz { get; set; } - public List? vi { get; set; } - public List? cy { get; set; } - public List? fy { get; set; } - public List? xh { get; set; } - public List? yi { get; set; } - public List? yo { get; set; } - public List? zu { get; set; } -} +namespace YoutubeLiveChatToDiscord.Models; public class Info { - public string? id { get; set; } - public string? title { get; set; } - public List? formats { get; set; } - public List? thumbnails { get; set; } - public string? thumbnail { get; set; } - public string? description { get; set; } - public string? upload_date { get; set; } - public string? uploader { get; set; } - public string? uploader_id { get; set; } - public string? uploader_url { get; set; } - public string? channel_id { get; set; } - public string? channel_url { get; set; } - public int view_count { get; set; } - public int age_limit { get; set; } - public string? webpage_url { get; set; } - public List? categories { get; set; } - public List? tags { get; set; } - public bool playable_in_embed { get; set; } - public bool is_live { get; set; } - public bool was_live { get; set; } - public string? live_status { get; set; } - public int release_timestamp { get; set; } - public Subtitles? subtitles { get; set; } - public int like_count { get; set; } - public string? channel { get; set; } - public int channel_follower_count { get; set; } - public string? availability { get; set; } - public string? webpage_url_basename { get; set; } - public string? extractor { get; set; } - public string? extractor_key { get; set; } - public string? display_id { get; set; } - public string? release_date { get; set; } - public string? fulltitle { get; set; } - public int epoch { get; set; } - public string? format_id { get; set; } - public string? url { get; set; } - public string? manifest_url { get; set; } - public double? tbr { get; set; } - public string? ext { get; set; } - public double? fps { get; set; } - public string? protocol { get; set; } - public int? quality { get; set; } - public int? width { get; set; } - public int? height { get; set; } - public string? vcodec { get; set; } - public string? acodec { get; set; } - public string? dynamic_range { get; set; } - public string? video_ext { get; set; } - public string? audio_ext { get; set; } - public double? vbr { get; set; } - public double? abr { get; set; } - public string? format { get; set; } - public string? resolution { get; set; } - public HttpHeaders? http_headers { get; set; } - public int? duration { get; set; } - public AutomaticCaptions? automatic_captions { get; set; } - public string? duration_string { get; set; } - public string? format_note { get; set; } - public int? filesize_approx { get; set; } - public int? asr { get; set; } + public class HttpHeaders + { + [JsonPropertyName("User-Agent")] + public string? UserAgent { get; set; } + public string? Accept { get; set; } + + [JsonPropertyName("Accept-Encoding")] + public string? AcceptEncoding { get; set; } + + [JsonPropertyName("Accept-Language")] + public string? AcceptLanguage { get; set; } + + [JsonPropertyName("Sec-Fetch-Mode")] + public string? SecFetchMode { get; set; } + } + + public class Fragment + { + public string? path { get; set; } + public double duration { get; set; } + } + + public class DownloaderOptions + { + public int http_chunk_size { get; set; } + } + + public class Format + { + public string? format_id { get; set; } + public string? url { get; set; } + public string? manifest_url { get; set; } + public double tbr { get; set; } + public string? ext { get; set; } + public double fps { get; set; } + public string? protocol { get; set; } + public int quality { get; set; } + public int width { get; set; } + public int height { get; set; } + public string? vcodec { get; set; } + public string? acodec { get; set; } + public string? dynamic_range { get; set; } + public string? video_ext { get; set; } + public string? audio_ext { get; set; } + public double vbr { get; set; } + public double abr { get; set; } + public string? format { get; set; } + public string? resolution { get; set; } + public HttpHeaders? http_headers { get; set; } + public string? format_note { get; set; } + public List? fragments { get; set; } + public int? asr { get; set; } + public long? filesize { get; set; } + public int? source_preference { get; set; } + public string? language { get; set; } + public int? language_preference { get; set; } + public DownloaderOptions? downloader_options { get; set; } + public string? container { get; set; } + public double? filesize_approx { get; set; } + } + + public class Thumbnail + { + public string? url { get; set; } + public int preference { get; set; } + public string? id { get; set; } + public int? height { get; set; } + public int? width { get; set; } + public string? resolution { get; set; } + } + + public class LiveChat + { + public string? url { get; set; } + public string? video_id { get; set; } + public string? ext { get; set; } + public string? protocol { get; set; } + } + + public class Subtitles + { + public List? live_chat { get; set; } + } + + public class Af + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Sq + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Am + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ar + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Hy + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Az + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Bn + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Eu + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Be + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class B + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Bg + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class My + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ca + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ceb + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class ZhHan + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class ZhHant + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Co + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Hr + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class C + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Da + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Nl + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class En + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Eo + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Et + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Fil + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Fi + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Fr + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Gl + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ka + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class De + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class El + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Gu + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ht + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ha + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Haw + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Iw + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Hi + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Hmn + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Hu + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Is + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ig + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Id + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ga + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class It + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ja + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Jv + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Kn + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Kk + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Km + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Rw + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ko + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ku + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ky + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Lo + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class La + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Lv + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Lt + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Lb + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Mk + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Mg + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class M + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ml + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Mt + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Mi + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Mr + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Mn + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ne + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class No + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ny + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Or + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class P + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Fa + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Pl + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Pt + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Pa + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ro + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ru + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Sm + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Gd + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Sr + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Sn + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Sd + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Si + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Sk + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Sl + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class So + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class St + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class E + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Su + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Sw + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Sv + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Tg + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ta + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Tt + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Te + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Th + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Tr + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Tk + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Uk + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ur + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Ug + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Uz + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Vi + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Cy + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Fy + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Xh + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Yi + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Yo + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class Zu + { + public string? ext { get; set; } + public string? url { get; set; } + public string? name { get; set; } + } + + public class AutomaticCaptions + { + public List? af { get; set; } + public List? sq { get; set; } + public List? am { get; set; } + public List? ar { get; set; } + public List? hy { get; set; } + public List? az { get; set; } + public List? bn { get; set; } + public List? eu { get; set; } + public List? be { get; set; } + public List? bs { get; set; } + public List? bg { get; set; } + public List? my { get; set; } + public List? ca { get; set; } + public List? ceb { get; set; } + + [JsonPropertyName("zh-Hans")] + public List? ZhHans { get; set; } + + [JsonPropertyName("zh-Hant")] + public List? ZhHant { get; set; } + public List? co { get; set; } + public List


? hr { get; set; } + public List? cs { get; set; } + public List? da { get; set; } + public List? nl { get; set; } + public List? en { get; set; } + public List? eo { get; set; } + public List? et { get; set; } + public List? fil { get; set; } + public List? fi { get; set; } + public List? fr { get; set; } + public List? gl { get; set; } + public List? ka { get; set; } + public List? de { get; set; } + public List? el { get; set; } + public List? gu { get; set; } + public List? ht { get; set; } + public List? ha { get; set; } + public List? haw { get; set; } + public List? iw { get; set; } + public List? hi { get; set; } + public List? hmn { get; set; } + public List? hu { get; set; } + public List? @is { get; set; } + public List? ig { get; set; } + public List? id { get; set; } + public List? ga { get; set; } + public List? it { get; set; } + public List? ja { get; set; } + public List? jv { get; set; } + public List? kn { get; set; } + public List? kk { get; set; } + public List? km { get; set; } + public List? rw { get; set; } + public List? ko { get; set; } + public List? ku { get; set; } + public List? ky { get; set; } + public List? lo { get; set; } + public List? la { get; set; } + public List? lv { get; set; } + public List? lt { get; set; } + public List? lb { get; set; } + public List? mk { get; set; } + public List? mg { get; set; } + public List? ms { get; set; } + public List? ml { get; set; } + public List? mt { get; set; } + public List? mi { get; set; } + public List? mr { get; set; } + public List? mn { get; set; } + public List? ne { get; set; } + public List? no { get; set; } + public List? ny { get; set; } + public List? or { get; set; } + public List

? ps { get; set; } + public List? fa { get; set; } + public List? pl { get; set; } + public List? pt { get; set; } + public List? pa { get; set; } + public List? ro { get; set; } + public List? ru { get; set; } + public List? sm { get; set; } + public List? gd { get; set; } + public List? sr { get; set; } + public List? sn { get; set; } + public List? sd { get; set; } + public List? si { get; set; } + public List? sk { get; set; } + public List? sl { get; set; } + public List? so { get; set; } + public List? st { get; set; } + public List? es { get; set; } + public List? su { get; set; } + public List? sw { get; set; } + public List? sv { get; set; } + public List? tg { get; set; } + public List? ta { get; set; } + public List? tt { get; set; } + public List? te { get; set; } + public List? th { get; set; } + public List? tr { get; set; } + public List? tk { get; set; } + public List? uk { get; set; } + public List? ur { get; set; } + public List? ug { get; set; } + public List? uz { get; set; } + public List? vi { get; set; } + public List? cy { get; set; } + public List? fy { get; set; } + public List? xh { get; set; } + public List? yi { get; set; } + public List? yo { get; set; } + public List? zu { get; set; } + } + +#pragma warning disable CS8981 // 類型名稱只包含小寫的 ASCII 字元。此類名稱可能保留供此語言使用。 + public class info +#pragma warning restore CS8981 // 類型名稱只包含小寫的 ASCII 字元。此類名稱可能保留供此語言使用。 + { + public string? id { get; set; } + public string? title { get; set; } + public List? formats { get; set; } + public List? thumbnails { get; set; } + public string? thumbnail { get; set; } + public string? description { get; set; } + public string? upload_date { get; set; } + public string? uploader { get; set; } + public string? uploader_id { get; set; } + public string? uploader_url { get; set; } + public string? channel_id { get; set; } + public string? channel_url { get; set; } + public int view_count { get; set; } + public int age_limit { get; set; } + public string? webpage_url { get; set; } + public List? categories { get; set; } + public List? tags { get; set; } + public bool playable_in_embed { get; set; } + public bool is_live { get; set; } + public bool was_live { get; set; } + public string? live_status { get; set; } + public int release_timestamp { get; set; } + public Subtitles? subtitles { get; set; } + public int like_count { get; set; } + public string? channel { get; set; } + public int channel_follower_count { get; set; } + public string? availability { get; set; } + public string? webpage_url_basename { get; set; } + public string? extractor { get; set; } + public string? extractor_key { get; set; } + public string? display_id { get; set; } + public string? release_date { get; set; } + public string? fulltitle { get; set; } + public int epoch { get; set; } + public string? format_id { get; set; } + public string? url { get; set; } + public string? manifest_url { get; set; } + public double? tbr { get; set; } + public string? ext { get; set; } + public double? fps { get; set; } + public string? protocol { get; set; } + public int? quality { get; set; } + public int? width { get; set; } + public int? height { get; set; } + public string? vcodec { get; set; } + public string? acodec { get; set; } + public string? dynamic_range { get; set; } + public string? video_ext { get; set; } + public string? audio_ext { get; set; } + public double? vbr { get; set; } + public double? abr { get; set; } + public string? format { get; set; } + public string? resolution { get; set; } + public HttpHeaders? http_headers { get; set; } + public int? duration { get; set; } + public AutomaticCaptions? automatic_captions { get; set; } + public string? duration_string { get; set; } + public string? format_note { get; set; } + public int? filesize_approx { get; set; } + public int? asr { get; set; } + } } -#pragma warning restore IDE1006 // 命名樣式 - diff --git a/Program.cs b/Program.cs index d7daae5..654c931 100644 --- a/Program.cs +++ b/Program.cs @@ -17,6 +17,7 @@ { services.AddHostedService() .AddSingleton() + .AddSingleton() .AddSingleton((service) => new DiscordWebhookClient(Environment.GetEnvironmentVariable("WEBHOOK"))); }) .Build(); diff --git a/Services/DiscordService.cs b/Services/DiscordService.cs new file mode 100644 index 0000000..aad8784 --- /dev/null +++ b/Services/DiscordService.cs @@ -0,0 +1,329 @@ +using Discord; +using Discord.Webhook; +using static YoutubeLiveChatToDiscord.Models.Chat; +using Chat = YoutubeLiveChatToDiscord.Models.Chat.chat; + +namespace YoutubeLiveChatToDiscord.Services; + +public class DiscordService +{ + private readonly ILogger _logger; + private readonly string _id; + private readonly DiscordWebhookClient _client; + + public DiscordService( + ILogger logger, + DiscordWebhookClient client) + { + _logger = logger; + _client = client; + _client.Log += DiscordWebhookClient_Log; + _id = Environment.GetEnvironmentVariable("VIDEO_ID") ?? ""; + if (string.IsNullOrEmpty(_id)) throw new ArgumentException(nameof(_id)); + } + + ///

+ /// 把.NET Core logger對應到Discord內建的logger上面 + /// + /// + /// + private Task DiscordWebhookClient_Log(LogMessage arg) + => Task.Run(() => + { + switch (arg.Severity) + { + case LogSeverity.Critical: + _logger.LogCritical("{message}", arg); + break; + case LogSeverity.Error: + _logger.LogError("{message}", arg); + break; + case LogSeverity.Warning: + _logger.LogWarning("{message}", arg); + break; + case LogSeverity.Info: + _logger.LogInformation("{message}", arg); + break; + case LogSeverity.Verbose: + _logger.LogTrace("{message}", arg); + break; + case LogSeverity.Debug: + default: + _logger.LogDebug("{message}", arg); + break; + } + }); + + /// + /// 建立Discord embed並送出至Webhook + /// + /// + /// + /// + /// 訊息格式未支援 + public async Task BuildRequestAndSendToDiscord(Chat chat, CancellationToken stoppingToken) + { + EmbedBuilder eb = new(); + eb.WithTitle(Environment.GetEnvironmentVariable("TITLE") ?? "") + .WithUrl($"https://youtu.be/{_id}") + .WithThumbnailUrl(Helper.GetOriginalImage(Environment.GetEnvironmentVariable("VIDEO_THUMB"))); + + var liveChatTextMessage = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatTextMessageRenderer; + var liveChatPaidMessage = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatPaidMessageRenderer; + var liveChatPaidSticker = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatPaidStickerRenderer; + var liveChatPurchaseSponsorshipsGift = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatSponsorshipsGiftPurchaseAnnouncementRenderer; + + // ReplaceChat: Treat as a new message + // This is rare and not easy to test. + // If it behaves strangely, please open a new issue with more examples. + var replaceChat = chat.replayChatItemAction?.actions?.FirstOrDefault()?.replaceChatItemAction?.replacementItem?.liveChatTextMessageRenderer; + if (null != replaceChat) + { + liveChatTextMessage = replaceChat; + } + + string author; + if (null != liveChatTextMessage) + { + BuildNormalMessage(ref eb, liveChatTextMessage, out author); + } + else if (null != liveChatPaidMessage) + // Super Chat + { + BuildSuperChatMessage(ref eb, liveChatPaidMessage, out author); + } + else if (null != liveChatPaidSticker) + // Super Chat Sticker + { + BuildSuperChatStickerMessage(ref eb, liveChatPaidSticker, out author); + } + else if (null != liveChatPurchaseSponsorshipsGift + && null != liveChatPurchaseSponsorshipsGift.header.liveChatSponsorshipsHeaderRenderer) + // Purchase Sponsorships Gift + { + BuildPurchaseSponsorshipsGiftMessage(ref eb, liveChatPurchaseSponsorshipsGift, out author); + } + // Discrad known garbage messages. + else if (IsGarbageMessage(chat)) { return; } + else + { + _logger.LogWarning("Message type not supported, skip sending to discord."); + throw new ArgumentException("Message type not supported", nameof(chat)); + } + + if (stoppingToken.IsCancellationRequested) return; + + await SendMessage(eb, author, stoppingToken); + + // The rate for Discord webhooks are 30 requests/minute per channel. + // Be careful when you run multiple instances in the same channel! + _logger.LogTrace("Wait 2 seconds for discord webhook rate limit"); + await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); + } + + private static bool IsGarbageMessage(Chat chat) => + // Banner Pinned message. + null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addBannerToLiveChatCommand + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.removeBannerForLiveChatCommand + // Click to show less. + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.showLiveChatTooltipCommand + // Welcome to live chat! Remember to guard your privacy and abide by our community guidelines. + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatViewerEngagementMessageRenderer + // Membership messages. + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatMembershipItemRenderer + // SC Ticker messages. + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addLiveChatTickerItemAction + // Delete messages. + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.markChatItemAsDeletedAction + // Remove Chat Item. Not really sure what this is. + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.removeChatItemAction + // Live chat mode change. + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatModeChangeMessageRenderer + // Poll + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.updateLiveChatPollAction + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.closeLiveChatActionPanelAction + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.showLiveChatActionPanelAction + // Sponsorships Gift redemption + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatSponsorshipsGiftRedemptionAnnouncementRenderer + // Have no idea what this is + || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatPlaceholderItemRenderer; + + private static EmbedBuilder BuildNormalMessage(ref EmbedBuilder eb, LiveChatTextMessageRenderer liveChatTextMessage, out string author) + { + List runs = liveChatTextMessage.message?.runs ?? new List(); + author = liveChatTextMessage.authorName?.simpleText ?? ""; + string authorPhoto = Helper.GetOriginalImage(liveChatTextMessage.authorPhoto?.thumbnails?.LastOrDefault()?.url); + + eb.WithDescription(string.Join("", runs.Select(p => p.text ?? (p.emoji?.searchTerms?.FirstOrDefault())))) + .WithAuthor(new EmbedAuthorBuilder().WithName(author) + .WithUrl($"https://www.youtube.com/channel/{liveChatTextMessage.authorExternalChannelId}") + .WithIconUrl(authorPhoto)); + + // Timestamp + long timeStamp = long.TryParse(liveChatTextMessage.timestampUsec, out long l) ? l / 1000 : 0; + EmbedFooterBuilder ft = new(); + string authorBadgeUrl = Helper.GetOriginalImage(liveChatTextMessage.authorBadges?.FirstOrDefault()?.liveChatAuthorBadgeRenderer?.customThumbnail?.thumbnails?.LastOrDefault()?.url); + ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp) + .LocalDateTime + .ToString("yyyy/MM/dd HH:mm:ss")) + .WithIconUrl(authorBadgeUrl); + + // From Stream Owner + //if (liveChatTextMessage.authorBadges?[0].liveChatAuthorBadgeRenderer?.icon?.iconType == "OWNER") + if (liveChatTextMessage.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID")) + { + eb.WithColor(Color.Gold); + ft.WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/crown.png"); + } + + eb.WithFooter(ft); + return eb; + } + + private static EmbedBuilder BuildSuperChatMessage(ref EmbedBuilder eb, LiveChatPaidMessageRenderer liveChatPaidMessage, out string author) + { + List runs = liveChatPaidMessage.message?.runs ?? new List(); + + author = liveChatPaidMessage.authorName?.simpleText ?? ""; + string authorPhoto = Helper.GetOriginalImage(liveChatPaidMessage.authorPhoto?.thumbnails?.LastOrDefault()?.url); + + eb.WithDescription(string.Join("", runs.Select(p => p.text ?? (p.emoji?.searchTerms?.FirstOrDefault())))) + .WithAuthor(new EmbedAuthorBuilder().WithName(author) + .WithUrl($"https://www.youtube.com/channel/{liveChatPaidMessage.authorExternalChannelId}") + .WithIconUrl(authorPhoto)); + + // Super Chat Amount + eb.WithFields(new EmbedFieldBuilder[] { new EmbedFieldBuilder().WithName("Amount").WithValue(liveChatPaidMessage.purchaseAmountText?.simpleText) }); + + // Super Chat Background Color + Color bgColor = (Color)System.Drawing.ColorTranslator.FromHtml(string.Format("#{0:X}", liveChatPaidMessage.bodyBackgroundColor)); + eb.WithColor(bgColor); + + // Timestamp + long timeStamp = long.TryParse(liveChatPaidMessage.timestampUsec, out long l) ? l / 1000 : 0; + EmbedFooterBuilder ft = new(); + ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp) + .LocalDateTime + .ToString("yyyy/MM/dd HH:mm:ss")) + .WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/wallet.png"); + + // From Stream Owner + if (liveChatPaidMessage.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID")) + { + eb.WithColor(Color.Gold); + ft.WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/crown.png"); + } + + eb.WithFooter(ft); + return eb; + } + + private static EmbedBuilder BuildSuperChatStickerMessage(ref EmbedBuilder eb, LiveChatPaidStickerRenderer liveChatPaidSticker, out string author) + { + author = liveChatPaidSticker.authorName?.simpleText ?? ""; + string authorPhoto = Helper.GetOriginalImage(liveChatPaidSticker.authorPhoto?.thumbnails?.LastOrDefault()?.url); + + eb.WithDescription("") + .WithAuthor(new EmbedAuthorBuilder().WithName(author) + .WithUrl($"https://www.youtube.com/channel/{liveChatPaidSticker.authorExternalChannelId}") + .WithIconUrl(authorPhoto)); + + // Super Chat Amount + eb.WithFields(new EmbedFieldBuilder[] { new EmbedFieldBuilder().WithName("Amount").WithValue(liveChatPaidSticker.purchaseAmountText?.simpleText) }); + + // Super Chat Background Color + Color bgColor = (Color)System.Drawing.ColorTranslator.FromHtml(string.Format("#{0:X}", liveChatPaidSticker.backgroundColor)); + eb.WithColor(bgColor); + + // Super Chat Sticker Picture + string stickerThumbUrl = Helper.GetOriginalImage("https:" + liveChatPaidSticker.sticker?.thumbnails?.LastOrDefault()?.url); + eb.WithThumbnailUrl(stickerThumbUrl); + + // Timestamp + long timeStamp = long.TryParse(liveChatPaidSticker.timestampUsec, out long l) ? l / 1000 : 0; + EmbedFooterBuilder ft = new(); + ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp) + .LocalDateTime + .ToString("yyyy/MM/dd HH:mm:ss")) + .WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/wallet.png"); + + // From Stream Owner + if (liveChatPaidSticker.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID")) + { + eb.WithColor(Color.Gold); + ft.WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/crown.png"); + } + + eb.WithFooter(ft); + return eb; + } + + private static EmbedBuilder BuildPurchaseSponsorshipsGiftMessage(ref EmbedBuilder eb, LiveChatSponsorshipsGiftPurchaseAnnouncementRenderer liveChatPurchaseSponsorshipsGift, out string author) + { + LiveChatSponsorshipsHeaderRenderer header = liveChatPurchaseSponsorshipsGift.header.liveChatSponsorshipsHeaderRenderer; + author = header.authorName?.simpleText ?? ""; + string authorPhoto = Helper.GetOriginalImage(header.authorPhoto?.thumbnails?.LastOrDefault()?.url); + + eb.WithDescription("") + .WithAuthor(new EmbedAuthorBuilder().WithName(author) + .WithUrl($"https://www.youtube.com/channel/{liveChatPurchaseSponsorshipsGift?.authorExternalChannelId}") + .WithIconUrl(authorPhoto)); + + // Gift Amount + eb.WithFields(new EmbedFieldBuilder[] { new EmbedFieldBuilder().WithName("Amount").WithValue(header?.primaryText?.runs?[1].text) }); + + // Gift Background Color + Color bgColor = (Color)System.Drawing.ColorTranslator.FromHtml("#0f9d58"); + eb.WithColor(bgColor); + + // Gift Picture + string? giftThumbUrl = header?.image?.thumbnails?.LastOrDefault()?.url; + if (null != giftThumbUrl) eb.WithThumbnailUrl(giftThumbUrl); + + // Timestamp + long timeStamp = long.TryParse(liveChatPurchaseSponsorshipsGift?.timestampUsec, out long l) ? l / 1000 : 0; + EmbedFooterBuilder ft = new(); + ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp) + .LocalDateTime + .ToString("yyyy/MM/dd HH:mm:ss")) + .WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/wallet.png"); + + // From Stream Owner + if (liveChatPurchaseSponsorshipsGift?.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID")) + { + //eb.WithColor(Color.Gold); + ft.WithIconUrl("https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/crown.png"); + } + + eb.WithFooter(ft); + return eb; + } + + private async Task SendMessage(EmbedBuilder eb, string author, CancellationToken cancellationToken) + { + _logger.LogDebug("Sending Request to Discord: {author}: {message}", author, eb.Description); + + try + { + await _send(); + } + catch (TimeoutException) { } + // System.Net.Http.HttpRequestException: Resource temporarily unavailable (discord.com:443) + catch (HttpRequestException) + { + // Retry once after 5 sec + await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); + await _send(); + } + + Task _send() + => _client.SendMessageAsync(embeds: new Embed[] { eb.Build() }) + .ContinueWith(p => + { +#pragma warning disable AsyncFixer02 // Long-running or blocking operations inside an async method + ulong messageId = p.Result; +#pragma warning restore AsyncFixer02 // Long-running or blocking operations inside an async method + _logger.LogDebug("Message sent to discord, message id: {messageId}", messageId); + }, cancellationToken); + } +} diff --git a/Services/LiveChatDownloadService.cs b/Services/LiveChatDownloadService.cs index 0e9c26e..39805e7 100644 --- a/Services/LiveChatDownloadService.cs +++ b/Services/LiveChatDownloadService.cs @@ -12,7 +12,6 @@ public class LiveChatDownloadService public LiveChatDownloadService(ILogger logger) { _logger = logger; - _id = Environment.GetEnvironmentVariable("VIDEO_ID") ?? ""; if (string.IsNullOrEmpty(_id)) throw new ArgumentException(nameof(_id)); } @@ -69,6 +68,5 @@ private Task ExecuteAsyncInternal(CancellationToken stoppingToken) live_chatOptionSet, stoppingToken)) .Unwrap(); - } }