? or { get; set; }
931 | public List? ps { get; set; }
932 | public List? fa { get; set; }
933 | public List? pl { get; set; }
934 | public List? pt { get; set; }
935 | public List? pa { get; set; }
936 | public List? ro { get; set; }
937 | public List? ru { get; set; }
938 | public List? sm { get; set; }
939 | public List? gd { get; set; }
940 | public List? sr { get; set; }
941 | public List? sn { get; set; }
942 | public List? sd { get; set; }
943 | public List? si { get; set; }
944 | public List? sk { get; set; }
945 | public List? sl { get; set; }
946 | public List? so { get; set; }
947 | public List? st { get; set; }
948 | public List? es { get; set; }
949 | public List? su { get; set; }
950 | public List? sw { get; set; }
951 | public List? sv { get; set; }
952 | public List? tg { get; set; }
953 | public List? ta { get; set; }
954 | public List? tt { get; set; }
955 | public List? te { get; set; }
956 | public List? th { get; set; }
957 | public List | ? tr { get; set; }
958 | public List? tk { get; set; }
959 | public List? uk { get; set; }
960 | public List? ur { get; set; }
961 | public List? ug { get; set; }
962 | public List? uz { get; set; }
963 | public List? vi { get; set; }
964 | public List? cy { get; set; }
965 | public List? fy { get; set; }
966 | public List? xh { get; set; }
967 | public List? yi { get; set; }
968 | public List? yo { get; set; }
969 | public List? zu { get; set; }
970 | }
971 |
972 | #pragma warning disable CS8981 // 類型名稱只包含小寫的 ASCII 字元。此類名稱可能保留供此語言使用。
973 | public class info
974 | #pragma warning restore CS8981 // 類型名稱只包含小寫的 ASCII 字元。此類名稱可能保留供此語言使用。
975 | {
976 | public string? id { get; set; }
977 | public string? title { get; set; }
978 | public List? formats { get; set; }
979 | public List? thumbnails { get; set; }
980 | public string? thumbnail { get; set; }
981 | public string? description { get; set; }
982 | public string? upload_date { get; set; }
983 | public string? uploader { get; set; }
984 | public string? uploader_id { get; set; }
985 | public string? uploader_url { get; set; }
986 | public string? channel_id { get; set; }
987 | public string? channel_url { get; set; }
988 | public int view_count { get; set; }
989 | public int age_limit { get; set; }
990 | public string? webpage_url { get; set; }
991 | public List? categories { get; set; }
992 | public List? tags { get; set; }
993 | public bool playable_in_embed { get; set; }
994 | public bool is_live { get; set; }
995 | public bool was_live { get; set; }
996 | public string? live_status { get; set; }
997 | public int release_timestamp { get; set; }
998 | public Subtitles? subtitles { get; set; }
999 | public int like_count { get; set; }
1000 | public string? channel { get; set; }
1001 | public int channel_follower_count { get; set; }
1002 | public string? availability { get; set; }
1003 | public string? webpage_url_basename { get; set; }
1004 | public string? extractor { get; set; }
1005 | public string? extractor_key { get; set; }
1006 | public string? display_id { get; set; }
1007 | public string? release_date { get; set; }
1008 | public string? fulltitle { get; set; }
1009 | public int epoch { get; set; }
1010 | public string? format_id { get; set; }
1011 | public string? url { get; set; }
1012 | public string? manifest_url { get; set; }
1013 | public double? tbr { get; set; }
1014 | public string? ext { get; set; }
1015 | public double? fps { get; set; }
1016 | public string? protocol { get; set; }
1017 | public int? quality { get; set; }
1018 | public int? width { get; set; }
1019 | public int? height { get; set; }
1020 | public string? vcodec { get; set; }
1021 | public string? acodec { get; set; }
1022 | public string? dynamic_range { get; set; }
1023 | public string? video_ext { get; set; }
1024 | public string? audio_ext { get; set; }
1025 | public double? vbr { get; set; }
1026 | public double? abr { get; set; }
1027 | public string? format { get; set; }
1028 | public string? resolution { get; set; }
1029 | public HttpHeaders? http_headers { get; set; }
1030 | public int? duration { get; set; }
1031 | public AutomaticCaptions? automatic_captions { get; set; }
1032 | public string? duration_string { get; set; }
1033 | public string? format_note { get; set; }
1034 | public int? filesize_approx { get; set; }
1035 | public int? asr { get; set; }
1036 | }
1037 | }
1038 |
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using Discord.Webhook;
2 | using YoutubeLiveChatToDiscord;
3 | using YoutubeLiveChatToDiscord.Services;
4 |
5 | Environment.SetEnvironmentVariable("VIDEO_ID", Environment.GetCommandLineArgs()[1]);
6 | Environment.SetEnvironmentVariable("WEBHOOK", Environment.GetCommandLineArgs()[2]);
7 |
8 | IEnumerable oldFiles = Directory.GetFiles(Directory.GetCurrentDirectory())
9 | .Where(p => p.Contains($"{Environment.GetEnvironmentVariable("VIDEO_ID")}.live_chat.json"));
10 | foreach (var file in oldFiles)
11 | {
12 | File.Delete(file);
13 | }
14 |
15 | IHost host = Host.CreateDefaultBuilder(args)
16 | .ConfigureServices(services =>
17 | {
18 | services.AddHostedService()
19 | .AddSingleton()
20 | .AddSingleton()
21 | .AddSingleton((service) => new DiscordWebhookClient(Environment.GetEnvironmentVariable("WEBHOOK")));
22 | })
23 | .Build();
24 |
25 | await host.RunAsync();
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Youtube Live Chat To Discord
2 |
3 | [](https://www.codefactor.io/repository/github/jim60105/youtubelivechattodiscord/overview/master) [](https://app.fossa.com/projects/git%2Bgithub.com%2Fjim60105%2FYoutubeLiveChatToDiscord?ref=badge_small)
4 |
5 | > [!CAUTION]
6 | > Please take note of the **AGPLv3** license that we are using.
7 | > You _**MUST**_ share **the source code** with **anyone who can access the services** (service, which means the Discord messages published by this program).
8 | > Share the URL of this GitHub repository, or publish the modified source code if any changes were made.
9 |
10 | ## Stream Youtube chat to Discord Webhook
11 |
12 | | Youtube Live Chat | | Discord Webhook |
13 | | :-----------------------------------------------------------------------------------------------------------------: | :-: | :-----------------------------------------------------------------------------------------------------------------: |
14 | |  | ➡️ |  |
15 | |  | ➡️ |  |
16 | |  | ➡️ |  |
17 | |  | ➡️ |  |
18 | |  | ➡️ |  |
19 | |  | ➡️ |  |
20 |
21 |
22 | English |
23 |
24 | 中文
25 |
26 |
27 |
28 | - The underlying implementation uses yt-dlp instead of the YouTube API, so there is no API quota limit.
29 | - When this tool is idle, it reads the JSON file generated by yt-dlp every 10 seconds.
30 | - Upon startup, it waits for 1 minute to skip old chats before starting monitoring.
31 | > If you want to skip this waiting and start immediately, please pass the environment variable `SKIP_STARTUP_WAITING`.
32 | - It can monitor membership-only live streams by automatically detect and import the `cookies.txt` file in the execution directory into yt-dlp.
33 | - It is not suitable for for scenarios with a high message speed.
34 | It sends a maximum of one Discord webhook every two seconds, which may cause delays if the new chat speed exceeds the forwarding speed.
35 | > Discord has a limitation that allows calling webhooks up to 30 times per minute in the same channel [ref](https://twitter.com/lolpython/status/967621046277820416).
36 | > If multiple instances of this tool are simultaneously running and pushed to the same channel, it's easy to trigger Discord cooldown. Please be aware of your usage environment.
37 |
38 | ## Membership-only (login required) videos
39 |
40 | If a file named `cookies.txt` exists in the program's execution directory, it will be used automatically.
41 |
42 | For Docker, please mount `cookies.txt` to `/app/cookies.txt`.
43 |
44 | ## Docker
45 |
46 | > Please refer to `docker-compose.yml`.
47 |
48 | Two parameters need to be passed in:
49 |
50 | - Video ID
51 | - Discord Webhook URL
52 |
53 | ```sh
54 | docker run --rm ghcr.io/jim60105/youtubelivechattodiscord [Video_Id] [Discord_Webhook_Url]
55 | ```
56 |
57 | Also available at [quay.io](https://quay.io/jim60105/youtubelivechattodiscord)
58 |
59 | ## Kubernetes Helm Chart
60 |
61 | ```sh
62 | git clone https://github.com/jim60105/YoutubeLiveChatToDiscord.git
63 | cd YoutubeLiveChatToDiscord/helm-chart
64 | vim values.yaml
65 | helm install [Release_Name] .
66 | ```
67 |
68 | ### Timezone
69 |
70 | Default timezone is `Asia/Taipei`. Please change it with `TZ` environment variable.
71 |
72 | ## LICENSE
73 |
74 | [](LICENSE)
75 |
76 | [GNU AFFERO GENERAL PUBLIC LICENSE Version 3](LICENSE)
77 |
78 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
79 |
80 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
81 |
82 | You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
83 |
84 | > [!CAUTION]
85 | > Please take note of the **AGPLv3** license that we are using.
86 | > You _**MUST**_ share **the source code** with **anyone who can access the services** (the Discord messages published by this program).
87 | > Share the URL of this GitHub repository, or publish the modified source code if any changes were made.
88 |
--------------------------------------------------------------------------------
/README.zh.md:
--------------------------------------------------------------------------------
1 | # Youtube Live Chat To Discord
2 |
3 | [](https://www.codefactor.io/repository/github/jim60105/youtubelivechattodiscord/overview/master) [](https://app.fossa.com/projects/git%2Bgithub.com%2Fjim60105%2FYoutubeLiveChatToDiscord?ref=badge_small)
4 |
5 | > [!CAUTION]
6 | > 請留意我所使用的 **AGPLv3** 授權條款。
7 | > 你 _**必須**_ 將 **原始碼** 公開給 **任何能存取到服務的人** (服務,也就是指此程式所發布的 Discord 訊息)。
8 | > 請分享此 GitHub 儲存庫的網址,或是公開修改過的原始碼。
9 |
10 | ## 將 Youtube 聊天室串流至 Discord Webhook
11 |
12 | | Youtube Live Chat | | Discord Webhook |
13 | | :-----------------------------------------------------------------------------------------------------------------: | :-: | :-----------------------------------------------------------------------------------------------------------------: |
14 | |  | ➡️ |  |
15 | |  | ➡️ |  |
16 | |  | ➡️ |  |
17 | |  | ➡️ |  |
18 | |  | ➡️ |  |
19 | |  | ➡️ |  |
20 |
21 |
22 |
23 | English
24 | |
25 | 中文
26 |
27 |
28 | - 底層使用 yt-dlp 而不是 youtube api 實作,因此沒有 API 額度限制
29 | - 此工具在閒置時,會以 10 秒為間隔讀取 yt-dlp 產出的 json 檔案
30 | - 剛啟動時會等待 1 分鐘跳過舊留言,再由此開始監控
31 | > 如果要跳過此等待即時啟動,請傳入環境變數 `SKIP_STARTUP_WAITING`
32 | - 它可以監控會員限定直播,會自動檢測執行目錄下的 `cookies.txt` 並將其匯入 yt-dlp
33 | - 不適合用在有大量留言的狀況,此工具是設計來監控 FreeChat
34 | 它最高每兩秒打一次 discord webhook ,可能會造成轉送速度跟不上留言速度
35 | > Discord 方面的限制為,同一頻道中每分鐘可呼叫 Webhook 30 次 [ref](https://twitter.com/lolpython/status/967621046277820416)
36 | > 若同時啟動複數此工具並推送至同一個頻道,很容易觸發 Discord 冷卻,請留意你的使用環境
37 |
38 | ## 會員限定 (需登入) 的影片
39 |
40 | 在程式的執行目錄若存在名為 `cookies.txt` 的檔案,它會自動使用
41 |
42 | Docker 請將 `cookies.txt` mount 至 `/app/cookies.txt`
43 |
44 | ## Docker
45 |
46 | > 請參考 `docker-compose.yml`
47 |
48 | 需傳入兩個參數
49 |
50 | - 影片 ID
51 | - Discord Webhook 網址
52 |
53 | ```sh
54 | docker run --rm ghcr.io/jim60105/youtubelivechattodiscord [Video_Id] [Discord_Webhook_Url]
55 | ```
56 |
57 | 也可在[quay.io](https://quay.io/jim60105/youtubelivechattodiscord)取得。
58 |
59 | ## Kubernetes Helm Chart
60 |
61 | ```sh
62 | git clone https://github.com/jim60105/YoutubeLiveChatToDiscord.git
63 | cd YoutubeLiveChatToDiscord/helm-chart
64 | vim values.yaml
65 | helm install [Release_Name] .
66 | ```
67 |
68 | ### Timezone
69 |
70 | 預設時區為 `Asia/Taipei`。請使用 `TZ` 環境變數進行更改。
71 |
72 | ## LICENSE
73 |
74 | [](LICENSE)
75 |
76 | [GNU AFFERO GENERAL PUBLIC LICENSE Version 3](LICENSE)
77 |
78 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
79 |
80 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
81 |
82 | You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
83 |
84 | > [!CAUTION]
85 | > 請留意我們使用的 **AGPLv3** 授權條款。
86 | > 你 _**必須**_ 將 **原始碼** 公開給 **任何能存取到服務的人** (也就是此程式所發布的 Discord 訊息)。
87 | > 請分享此 GitHub 儲存庫的網址,或是公開修改過的原始碼。
88 |
--------------------------------------------------------------------------------
/Services/DiscordService.cs:
--------------------------------------------------------------------------------
1 | using Discord;
2 | using Discord.Webhook;
3 | using static YoutubeLiveChatToDiscord.Models.Chat;
4 | using Chat = YoutubeLiveChatToDiscord.Models.Chat.chat;
5 |
6 | namespace YoutubeLiveChatToDiscord.Services;
7 |
8 | public class DiscordService
9 | {
10 | private readonly ILogger _logger;
11 | private readonly string _id;
12 | private readonly DiscordWebhookClient _client;
13 | private static readonly Color _ownerColor = new(0xffd600);
14 | private static readonly Color _sponsorColor = new(0x0f9d58);
15 | private static readonly string _crownIcon = "https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/crown.png";
16 | private static readonly string _walletIcon = "https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/wallet.png";
17 | private static readonly string _giftIcon = "https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/master/assets/gift.png";
18 |
19 | public DiscordService(
20 | ILogger logger,
21 | DiscordWebhookClient client)
22 | {
23 | _logger = logger;
24 | _client = client;
25 | _client.Log += DiscordWebhookClient_Log;
26 | _id = Environment.GetEnvironmentVariable("VIDEO_ID") ?? "";
27 | if (string.IsNullOrEmpty(_id)) throw new ArgumentException(nameof(_id));
28 | }
29 |
30 | ///
31 | /// 把.NET Core logger對應到Discord內建的logger上面
32 | ///
33 | ///
34 | ///
35 | private Task DiscordWebhookClient_Log(LogMessage arg)
36 | => Task.Run(() =>
37 | {
38 | switch (arg.Severity)
39 | {
40 | case LogSeverity.Critical:
41 | _logger.LogCritical("{message}", arg);
42 | break;
43 | case LogSeverity.Error:
44 | _logger.LogError("{message}", arg);
45 | break;
46 | case LogSeverity.Warning:
47 | _logger.LogWarning("{message}", arg);
48 | break;
49 | case LogSeverity.Info:
50 | _logger.LogInformation("{message}", arg);
51 | break;
52 | case LogSeverity.Verbose:
53 | _logger.LogTrace("{message}", arg);
54 | break;
55 | case LogSeverity.Debug:
56 | default:
57 | _logger.LogDebug("{message}", arg);
58 | break;
59 | }
60 | });
61 |
62 | ///
63 | /// 建立Discord embed並送出至Webhook
64 | ///
65 | ///
66 | ///
67 | ///
68 | /// 訊息格式未支援
69 | public async Task BuildRequestAndSendToDiscord(Chat chat, CancellationToken stoppingToken)
70 | {
71 | EmbedBuilder eb = new();
72 | eb.WithTitle(Environment.GetEnvironmentVariable("TITLE") ?? "")
73 | .WithUrl($"https://youtu.be/{_id}")
74 | .WithThumbnailUrl(Helper.GetOriginalImage(Environment.GetEnvironmentVariable("VIDEO_THUMB")));
75 |
76 | var liveChatTextMessage = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatTextMessageRenderer;
77 | var liveChatPaidMessage = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatPaidMessageRenderer;
78 | var liveChatPaidSticker = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatPaidStickerRenderer;
79 | var liveChatMembershipItemRenderer = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatMembershipItemRenderer;
80 | var liveChatPurchaseSponsorshipsGift = chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatSponsorshipsGiftPurchaseAnnouncementRenderer;
81 |
82 | // ReplaceChat: Treat as a new message
83 | // This is rare and not easy to test.
84 | // If it behaves strangely, please open a new issue with more examples.
85 | var replaceChat = chat.replayChatItemAction?.actions?.FirstOrDefault()?.replaceChatItemAction?.replacementItem?.liveChatTextMessageRenderer;
86 | if (null != replaceChat)
87 | {
88 | liveChatTextMessage = replaceChat;
89 | }
90 |
91 | string author;
92 | if (null != liveChatTextMessage)
93 | {
94 | BuildNormalMessage(ref eb, liveChatTextMessage, out author);
95 | }
96 | else if (null != liveChatPaidMessage)
97 | // Super Chat
98 | {
99 | BuildSuperChatMessage(ref eb, liveChatPaidMessage, out author);
100 | }
101 | else if (null != liveChatPaidSticker)
102 | // Super Chat Sticker
103 | {
104 | BuildSuperChatStickerMessage(ref eb, liveChatPaidSticker, out author);
105 | }
106 | else if (null != liveChatMembershipItemRenderer)
107 | // Join Membership
108 | {
109 | BuildMemberShipMessage(ref eb, liveChatMembershipItemRenderer, out author);
110 | }
111 | else if (null != liveChatPurchaseSponsorshipsGift
112 | && null != liveChatPurchaseSponsorshipsGift.header.liveChatSponsorshipsHeaderRenderer)
113 | // Purchase Sponsorships Gift
114 | {
115 | BuildPurchaseSponsorshipsGiftMessage(ref eb, liveChatPurchaseSponsorshipsGift, out author);
116 | }
117 | // Discrad known garbage messages.
118 | else if (IsGarbageMessage(chat)) { return; }
119 | else
120 | {
121 | _logger.LogWarning("Message type not supported, skip sending to discord.");
122 | throw new ArgumentException("Message type not supported", nameof(chat));
123 | }
124 |
125 | if (stoppingToken.IsCancellationRequested) return;
126 |
127 | await SendMessage(eb, author, stoppingToken);
128 |
129 | // The rate for Discord webhooks are 30 requests/minute per channel.
130 | // Be careful when you run multiple instances in the same channel!
131 | _logger.LogTrace("Wait 2 seconds for discord webhook rate limit");
132 | await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
133 | }
134 |
135 | private static bool IsGarbageMessage(Chat chat) =>
136 | // Banner Pinned message.
137 | null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addBannerToLiveChatCommand
138 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.removeBannerForLiveChatCommand
139 | // Click to show less.
140 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.showLiveChatTooltipCommand
141 | // Welcome to live chat! Remember to guard your privacy and abide by our community guidelines.
142 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatViewerEngagementMessageRenderer
143 | // SC Ticker messages.
144 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addLiveChatTickerItemAction
145 | // Delete messages.
146 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.markChatItemAsDeletedAction
147 | // Remove Chat Item. Not really sure what this is.
148 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.removeChatItemAction
149 | // Live chat mode change.
150 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatModeChangeMessageRenderer
151 | // Poll
152 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.updateLiveChatPollAction
153 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.closeLiveChatActionPanelAction
154 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.showLiveChatActionPanelAction
155 | // Sponsorships Gift redemption
156 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatSponsorshipsGiftRedemptionAnnouncementRenderer
157 | // Have no idea what this is
158 | || null != chat.replayChatItemAction?.actions?.FirstOrDefault()?.addChatItemAction?.item?.liveChatPlaceholderItemRenderer;
159 |
160 | private static EmbedBuilder BuildNormalMessage(ref EmbedBuilder eb, LiveChatTextMessageRenderer liveChatTextMessage, out string author)
161 | {
162 | List runs = liveChatTextMessage.message?.runs ?? new List();
163 | author = liveChatTextMessage.authorName?.simpleText ?? "";
164 | string authorPhoto = Helper.GetOriginalImage(liveChatTextMessage.authorPhoto?.thumbnails?.LastOrDefault()?.url);
165 |
166 | eb.WithDescription(string.Join("", runs.Select(p => p.text ?? (p.emoji?.searchTerms?.FirstOrDefault()))))
167 | .WithAuthor(new EmbedAuthorBuilder().WithName(author)
168 | .WithUrl($"https://www.youtube.com/channel/{liveChatTextMessage.authorExternalChannelId}")
169 | .WithIconUrl(authorPhoto));
170 |
171 | // Timestamp
172 | long timeStamp = long.TryParse(liveChatTextMessage.timestampUsec, out long l) ? l / 1000 : 0;
173 | EmbedFooterBuilder ft = new();
174 | string authorBadgeUrl = Helper.GetOriginalImage(liveChatTextMessage.authorBadges?.FirstOrDefault()?.liveChatAuthorBadgeRenderer?.customThumbnail?.thumbnails?.LastOrDefault()?.url);
175 | ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp)
176 | .LocalDateTime
177 | .ToString("yyyy/MM/dd HH:mm:ss"))
178 | .WithIconUrl(authorBadgeUrl);
179 |
180 | // From Stream Owner
181 | if (liveChatTextMessage.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID"))
182 | {
183 | eb.WithColor(_ownerColor);
184 | ft.WithIconUrl(_crownIcon);
185 | }
186 |
187 | eb.WithFooter(ft);
188 | return eb;
189 | }
190 |
191 | private static EmbedBuilder BuildSuperChatMessage(ref EmbedBuilder eb, LiveChatPaidMessageRenderer liveChatPaidMessage, out string author)
192 | {
193 | List runs = liveChatPaidMessage.message?.runs ?? new List();
194 |
195 | author = liveChatPaidMessage.authorName?.simpleText ?? "";
196 | string authorPhoto = Helper.GetOriginalImage(liveChatPaidMessage.authorPhoto?.thumbnails?.LastOrDefault()?.url);
197 |
198 | eb.WithDescription(string.Join("", runs.Select(p => p.text ?? (p.emoji?.searchTerms?.FirstOrDefault()))))
199 | .WithAuthor(new EmbedAuthorBuilder().WithName(author)
200 | .WithUrl($"https://www.youtube.com/channel/{liveChatPaidMessage.authorExternalChannelId}")
201 | .WithIconUrl(authorPhoto));
202 |
203 | // Super Chat Amount
204 | eb.WithFields(new EmbedFieldBuilder[] { new EmbedFieldBuilder().WithName("Amount").WithValue(liveChatPaidMessage.purchaseAmountText?.simpleText) });
205 |
206 | // Super Chat Background Color
207 | Color bgColor = (Color)System.Drawing.ColorTranslator.FromHtml(Helper.YoutubeColorConverter(liveChatPaidMessage.bodyBackgroundColor));
208 | eb.WithColor(bgColor);
209 |
210 | // Lower Bumper
211 | eb = AppendLowerBumper(ref eb, liveChatPaidMessage.lowerBumper);
212 |
213 | // Timestamp
214 | long timeStamp = long.TryParse(liveChatPaidMessage.timestampUsec, out long l) ? l / 1000 : 0;
215 | EmbedFooterBuilder ft = new();
216 | ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp)
217 | .LocalDateTime
218 | .ToString("yyyy/MM/dd HH:mm:ss"))
219 | .WithIconUrl(_walletIcon);
220 |
221 | // From Stream Owner
222 | if (liveChatPaidMessage.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID"))
223 | {
224 | //eb.WithColor(_ownerColor);
225 | ft.WithIconUrl(_crownIcon);
226 | }
227 |
228 | eb.WithFooter(ft);
229 | return eb;
230 | }
231 |
232 | private static EmbedBuilder BuildSuperChatStickerMessage(ref EmbedBuilder eb, LiveChatPaidStickerRenderer liveChatPaidSticker, out string author)
233 | {
234 | author = liveChatPaidSticker.authorName?.simpleText ?? "";
235 | string authorPhoto = Helper.GetOriginalImage(liveChatPaidSticker.authorPhoto?.thumbnails?.LastOrDefault()?.url);
236 |
237 | eb.WithDescription("")
238 | .WithAuthor(new EmbedAuthorBuilder().WithName(author)
239 | .WithUrl($"https://www.youtube.com/channel/{liveChatPaidSticker.authorExternalChannelId}")
240 | .WithIconUrl(authorPhoto));
241 |
242 | // Super Chat Amount
243 | eb.WithFields(new EmbedFieldBuilder[] { new EmbedFieldBuilder().WithName("Amount").WithValue(liveChatPaidSticker.purchaseAmountText?.simpleText) });
244 |
245 | // Super Chat Background Color
246 | Color bgColor = (Color)System.Drawing.ColorTranslator.FromHtml(Helper.YoutubeColorConverter(liveChatPaidSticker.backgroundColor));
247 | eb.WithColor(bgColor);
248 |
249 | // Super Chat Sticker Picture
250 | string stickerThumbUrl = Helper.GetOriginalImage("https:" + liveChatPaidSticker.sticker?.thumbnails?.LastOrDefault()?.url);
251 | eb.WithThumbnailUrl(stickerThumbUrl);
252 |
253 | // Lower Bumper
254 | eb = AppendLowerBumper(ref eb, liveChatPaidSticker.lowerBumper);
255 |
256 | // Timestamp
257 | long timeStamp = long.TryParse(liveChatPaidSticker.timestampUsec, out long l) ? l / 1000 : 0;
258 | EmbedFooterBuilder ft = new();
259 | ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp)
260 | .LocalDateTime
261 | .ToString("yyyy/MM/dd HH:mm:ss"))
262 | .WithIconUrl(_walletIcon);
263 |
264 | // From Stream Owner
265 | if (liveChatPaidSticker.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID"))
266 | {
267 | //eb.WithColor(_ownerColor);
268 | ft.WithIconUrl(_crownIcon);
269 | }
270 |
271 | eb.WithFooter(ft);
272 | return eb;
273 | }
274 |
275 | private static EmbedBuilder BuildMemberShipMessage(ref EmbedBuilder eb, LiveChatMembershipItemRenderer liveChatMembershipItemRenderer, out string author)
276 | {
277 | List? header = liveChatMembershipItemRenderer.headerPrimaryText?.runs
278 | ?? liveChatMembershipItemRenderer.headerSubtext?.runs;
279 | List? message = liveChatMembershipItemRenderer.message?.runs;
280 |
281 | author = liveChatMembershipItemRenderer.authorName?.simpleText ?? "";
282 | string authorPhoto = Helper.GetOriginalImage(liveChatMembershipItemRenderer.authorPhoto?.thumbnails?.LastOrDefault()?.url);
283 |
284 | if (null != message)
285 | {
286 | eb.WithDescription(string.Join("", (message ?? []).Select(p => p.text ?? (p.emoji?.searchTerms?.FirstOrDefault()))));
287 | if (null != header)
288 | {
289 | eb.WithFields(new EmbedFieldBuilder[]
290 | {
291 | new EmbedFieldBuilder().WithName("Header")
292 | .WithValue(string.Join("", header.Select(p => p.text ?? (p.emoji?.searchTerms?.FirstOrDefault()))))
293 | });
294 | }
295 | }
296 | else if (null != header)
297 | {
298 | eb.WithDescription(string.Join("", (header ?? []).Select(p => p.text ?? (p.emoji?.searchTerms?.FirstOrDefault()))));
299 | }
300 |
301 | eb.WithAuthor(new EmbedAuthorBuilder().WithName(author)
302 | .WithUrl($"https://www.youtube.com/channel/{liveChatMembershipItemRenderer.authorExternalChannelId}")
303 | .WithIconUrl(authorPhoto));
304 |
305 | // Membership Background Color
306 | eb.WithColor(_sponsorColor);
307 |
308 | // Timestamp
309 | long timeStamp = long.TryParse(liveChatMembershipItemRenderer.timestampUsec, out long l) ? l / 1000 : 0;
310 | EmbedFooterBuilder ft = new();
311 | string authorBadgeUrl = Helper.GetOriginalImage(liveChatMembershipItemRenderer.authorBadges?.FirstOrDefault()?.liveChatAuthorBadgeRenderer?.customThumbnail?.thumbnails?.LastOrDefault()?.url);
312 | ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp)
313 | .LocalDateTime
314 | .ToString("yyyy/MM/dd HH:mm:ss"))
315 | .WithIconUrl(authorBadgeUrl);
316 |
317 | eb.WithFooter(ft);
318 | return eb;
319 | }
320 |
321 | private static EmbedBuilder BuildPurchaseSponsorshipsGiftMessage(ref EmbedBuilder eb, LiveChatSponsorshipsGiftPurchaseAnnouncementRenderer liveChatPurchaseSponsorshipsGift, out string author)
322 | {
323 | LiveChatSponsorshipsHeaderRenderer header = liveChatPurchaseSponsorshipsGift.header.liveChatSponsorshipsHeaderRenderer;
324 | author = header.authorName?.simpleText ?? "";
325 | string authorPhoto = Helper.GetOriginalImage(header.authorPhoto?.thumbnails?.LastOrDefault()?.url);
326 |
327 | eb.WithDescription("")
328 | .WithAuthor(new EmbedAuthorBuilder().WithName(author)
329 | .WithUrl($"https://www.youtube.com/channel/{liveChatPurchaseSponsorshipsGift?.authorExternalChannelId}")
330 | .WithIconUrl(authorPhoto));
331 |
332 | // Gift Amount
333 | eb.WithFields(new EmbedFieldBuilder[] { new EmbedFieldBuilder().WithName("Amount").WithValue(header?.primaryText?.runs?[1].text) });
334 |
335 | // Gift Background Color
336 | eb.WithColor(_sponsorColor);
337 |
338 | // Gift Picture
339 | string? giftThumbUrl = header?.image?.thumbnails?.LastOrDefault()?.url;
340 | if (null != giftThumbUrl) eb.WithThumbnailUrl(giftThumbUrl);
341 |
342 | // Timestamp
343 | long timeStamp = long.TryParse(liveChatPurchaseSponsorshipsGift?.timestampUsec, out long l) ? l / 1000 : 0;
344 | EmbedFooterBuilder ft = new();
345 | ft.WithText(DateTimeOffset.FromUnixTimeMilliseconds(timeStamp)
346 | .LocalDateTime
347 | .ToString("yyyy/MM/dd HH:mm:ss"))
348 | .WithIconUrl(_giftIcon);
349 |
350 | // From Stream Owner
351 | if (liveChatPurchaseSponsorshipsGift?.authorExternalChannelId == Environment.GetEnvironmentVariable("CHANNEL_ID"))
352 | {
353 | //eb.WithColor(_ownerColor);
354 | ft.WithIconUrl(_crownIcon);
355 | }
356 |
357 | eb.WithFooter(ft);
358 | return eb;
359 | }
360 |
361 | private static EmbedBuilder AppendLowerBumper(ref EmbedBuilder eb, LowerBumper? lowerBumper)
362 | => null == lowerBumper || null == lowerBumper.liveChatItemBumperViewModel?.content?.bumperUserEduContentViewModel
363 | ? eb
364 | : eb.WithFields(new EmbedFieldBuilder[]
365 | {
366 | new EmbedFieldBuilder().WithName("LowerBumper")
367 | .WithValue(lowerBumper.liveChatItemBumperViewModel.content.bumperUserEduContentViewModel?.text?.content)
368 | });
369 |
370 | private async Task SendMessage(EmbedBuilder eb, string author, CancellationToken cancellationToken)
371 | {
372 | _logger.LogDebug("Sending Request to Discord: {author}: {message}", author, eb.Description);
373 |
374 | try
375 | {
376 | await _send();
377 | }
378 | catch (TimeoutException) { }
379 | // System.Net.Http.HttpRequestException: Resource temporarily unavailable (discord.com:443)
380 | catch (HttpRequestException)
381 | {
382 | // Retry once after 5 sec
383 | await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
384 | await _send();
385 | }
386 |
387 | Task _send()
388 | => _client.SendMessageAsync(embeds: new Embed[] { eb.Build() })
389 | .ContinueWith(p =>
390 | {
391 | #pragma warning disable AsyncFixer02 // Long-running or blocking operations inside an async method
392 | ulong messageId = p.Result;
393 | #pragma warning restore AsyncFixer02 // Long-running or blocking operations inside an async method
394 | _logger.LogDebug("Message sent to discord, message id: {messageId}", messageId);
395 | }, cancellationToken);
396 | }
397 | }
398 |
--------------------------------------------------------------------------------
/Services/LiveChatDownloadService.cs:
--------------------------------------------------------------------------------
1 | using YoutubeDLSharp;
2 | using YoutubeDLSharp.Options;
3 |
4 | namespace YoutubeLiveChatToDiscord.Services;
5 |
6 | public class LiveChatDownloadService
7 | {
8 | private readonly ILogger _logger;
9 | private readonly string _id;
10 | public Task downloadProcess = Task.FromResult(0);
11 |
12 | public LiveChatDownloadService(ILogger logger)
13 | {
14 | _logger = logger;
15 | _id = Environment.GetEnvironmentVariable("VIDEO_ID") ?? "";
16 | if (string.IsNullOrEmpty(_id)) throw new ArgumentException(nameof(_id));
17 | }
18 |
19 | public Task ExecuteAsync(CancellationToken stoppingToken)
20 | {
21 | downloadProcess = ExecuteAsyncInternal(stoppingToken);
22 | return downloadProcess;
23 | }
24 |
25 | private Task ExecuteAsyncInternal(CancellationToken stoppingToken)
26 | {
27 | OptionSet live_chatOptionSet = new()
28 | {
29 | IgnoreConfig = true,
30 | WriteSubs = true,
31 | SubLangs = "live_chat",
32 | SkipDownload = true,
33 | NoPart = true,
34 | NoContinue = true,
35 | Output = "%(id)s",
36 | IgnoreNoFormatsError = true
37 | };
38 |
39 | OptionSet info_jsonOptionSet = new()
40 | {
41 | IgnoreConfig = true,
42 | WriteInfoJson = true,
43 | SkipDownload = true,
44 | NoPart = true,
45 | Output = "%(id)s",
46 | IgnoreNoFormatsError = true
47 | };
48 |
49 | FileInfo cookies = new("cookies.txt");
50 | if (cookies.Exists)
51 | {
52 | _logger.LogInformation("Detected {cookies}, use it for yt-dlp", cookies.FullName);
53 | var bak = cookies.CopyTo("cookies.copy.txt", true);
54 | live_chatOptionSet.Cookies = bak.FullName;
55 | info_jsonOptionSet.Cookies = bak.FullName;
56 | }
57 |
58 | YoutubeDLProcess ytdlProc = new(Helper.WhereIsYt_dlp());
59 | ytdlProc.OutputReceived += (o, e) => _logger.LogTrace("{message}", e.Data);
60 | ytdlProc.ErrorReceived += (o, e) => _logger.LogError("{error}", e.Data);
61 |
62 | string url = $"https://www.youtube.com/watch?v={_id}";
63 | _logger.LogInformation("Start yt-dlp with url: {url}", url);
64 | return ytdlProc.RunAsync(new string[] { url },
65 | info_jsonOptionSet,
66 | stoppingToken)
67 | .ContinueWith((e) => ytdlProc.RunAsync(new string[] { url },
68 | live_chatOptionSet,
69 | stoppingToken))
70 | .Unwrap();
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/YoutubeLiveChatToDiscord.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | enable
5 | enable
6 | dotnet-LiveChatToDiscord-ACE24696-7DD5-4164-8805-CF76B90CBA6C
7 | false
8 | true
9 | true
10 | true
11 | true
12 | true
13 | Linux
14 | .
15 | debug
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/YoutubeLiveChatToDiscord.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.32112.339
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YoutubeLiveChatToDiscord", "YoutubeLiveChatToDiscord.csproj", "{FEDF1496-1E51-49BA-8C3B-FDD9856AECAC}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {FEDF1496-1E51-49BA-8C3B-FDD9856AECAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {FEDF1496-1E51-49BA-8C3B-FDD9856AECAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {FEDF1496-1E51-49BA-8C3B-FDD9856AECAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {FEDF1496-1E51-49BA-8C3B-FDD9856AECAC}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {46B345FA-6C68-4791-BF20-8D003B274F61}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "Microsoft.Hosting.Lifetime": "Information"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.Hosting.Lifetime": "Information"
6 | },
7 | "Console": {
8 | "DisableColors": true,
9 | "FormatterOptions": {
10 | "SingleLine": true
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/assets/crown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/97e48c13d0ab37ca741819c0ca21eb0c0ba1824e/assets/crown.png
--------------------------------------------------------------------------------
/assets/gift.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/97e48c13d0ab37ca741819c0ca21eb0c0ba1824e/assets/gift.png
--------------------------------------------------------------------------------
/assets/wallet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jim60105/YoutubeLiveChatToDiscord/97e48c13d0ab37ca741819c0ca21eb0c0ba1824e/assets/wallet.png
--------------------------------------------------------------------------------
/docker-compose.override.yml:
--------------------------------------------------------------------------------
1 | # Override logging settings to LogServer
2 | version: "3.7"
3 |
4 | x-logging:
5 | &default-logging
6 | driver: "gelf"
7 | options:
8 | gelf-address: "udp://127.0.0.1:12201"
9 |
10 | services:
11 | youtubelivechattodiscord:
12 | logging: *default-logging
13 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 |
3 | x-labels:
4 | labels: &default-label
5 | youtubelivechattodiscord:
6 | services:
7 | youtubelivechattodiscord:
8 | image: ghcr.io/jim60105/youtubelivechattodiscord
9 | # build: .
10 | labels: *default-label
11 | restart: on-failure:3 # yt-dlp is easyily to get stuck in a restart-failed loop during long-term downloads
12 | # volumes:
13 | # - ./appsettings.json:/app/appsettings.json
14 | # - ./cookies.txt:/app/cookies.txt
15 | # Youtube videoId, discord webhook url
16 | command: ["", ""]
17 |
18 | # Restart main container every hour.
19 | jobber:
20 | image: blacklabelops/jobber:docker
21 | restart: always
22 | volumes:
23 | - /var/run/docker.sock:/var/run/docker.sock:ro
24 | environment:
25 | - JOB_NAME1=start
26 | - JOB_COMMAND1=docker start $$(docker ps -aqf "label=youtubelivechattodiscord")
27 | - JOB_TIME1=0 0 * * * * #Every hour
28 | - JOB_NOTIFY_ERR1=false
29 | - JOB_NOTIFY_FAIL1=false
30 |
--------------------------------------------------------------------------------
/helm-chart/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/helm-chart/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: youtube-live-chat-to-discord
3 | description: Stream Youtube Chat to Discord Webhook
4 | # A chart can be either an 'application' or a 'library' chart.
5 | #
6 | # Application charts are a collection of templates that can be packaged into versioned archives
7 | # to be deployed.
8 | #
9 | # Library charts provide useful utilities or functions for the chart developer. They're included as
10 | # a dependency of application charts to inject those utilities and functions into the rendering
11 | # pipeline. Library charts do not define any templates and therefore cannot be deployed.
12 | type: application
13 | # This is the chart version. This version number should be incremented each time you make changes
14 | # to the chart and its templates, including the app version.
15 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
16 | version: 0.1.0
17 | # This is the version number of the application being deployed. This version number should be
18 | # incremented each time you make changes to the application. Versions are not expected to
19 | # follow Semantic Versioning. They should reflect the version the application is using.
20 | # It is recommended to use it with quotes.
21 | appVersion: '0.1.0'
22 |
--------------------------------------------------------------------------------
/helm-chart/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "youtube-live-chat-to-discord.name" -}}
5 | {{- default $.Chart.Name $.Values.nameOverride | trunc 63 | trimSuffix "-" }}
6 | {{- end }}
7 |
8 | {{/*
9 | Create a default fully qualified app name.
10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
11 | If release name contains chart name it will be used as a full name.
12 | */}}
13 | {{- define "youtube-live-chat-to-discord.fullname" -}}
14 | {{- if $.Values.fullnameOverride }}
15 | {{- $.Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
16 | {{- else }}
17 | {{- $name := default $.Chart.Name $.Values.nameOverride }}
18 | {{- if contains $name $.Release.Name }}
19 | {{- $.Release.Name | trunc 63 | trimSuffix "-" }}
20 | {{- else }}
21 | {{- printf "%s-%s" $.Release.Name $name | trunc 63 | trimSuffix "-" }}
22 | {{- end }}
23 | {{- end }}
24 | {{- end }}
25 |
26 | {{/*
27 | Create chart name and version as used by the chart label.
28 | */}}
29 | {{- define "youtube-live-chat-to-discord$.Chart" -}}
30 | {{- printf "%s-%s" $.Chart.Name $.Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
31 | {{- end }}
32 |
33 | {{/*
34 | Common labels
35 | */}}
36 | {{- define "youtube-live-chat-to-discord.labels" -}}
37 | helm.sh/chart: {{ include "youtube-live-chat-to-discord$.Chart" . }}
38 | {{ include "youtube-live-chat-to-discord.selectorLabels" . }}
39 | {{- if $.Chart.AppVersion }}
40 | app.kubernetes.io/version: {{ $.Chart.AppVersion | quote }}
41 | {{- end }}
42 | app.kubernetes.io/managed-by: {{ $.Release.Service }}
43 | {{- end }}
44 |
45 | {{/*
46 | Selector labels
47 | */}}
48 | {{- define "youtube-live-chat-to-discord.selectorLabels" -}}
49 | app.kubernetes.io/name: {{ include "youtube-live-chat-to-discord.name" . }}
50 | app.kubernetes.io/instance: {{ $.Release.Name }}
51 | {{- end }}
52 |
53 | {{/*
54 | Create the name of the service account to use
55 | */}}
56 | {{- define "youtube-live-chat-to-discord.serviceAccountName" -}}
57 | {{- if $.Values.serviceAccount.create }}
58 | {{- default (include "youtube-live-chat-to-discord.fullname" .) $.Values.serviceAccount.name }}
59 | {{- else }}
60 | {{- default "default" $.Values.serviceAccount.name }}
61 | {{- end }}
62 | {{- end }}
63 |
--------------------------------------------------------------------------------
/helm-chart/templates/configMap.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: {{ include "youtube-live-chat-to-discord.fullname" $ }}-cookies
5 | data:
6 | cookies.txt: {{- .Values.cookies | toYaml | indent 1 }}
7 |
--------------------------------------------------------------------------------
/helm-chart/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | {{- range .Values.deployments }}
2 | ---
3 | apiVersion: apps/v1
4 | kind: Deployment
5 | metadata:
6 | name: {{ include "youtube-live-chat-to-discord.fullname" $ }}-{{ .name }}
7 | labels:
8 | {{- include "youtube-live-chat-to-discord.labels" $ | nindent 4 }}
9 | spec:
10 | replicas: 1
11 | selector:
12 | matchLabels:
13 | app: {{ .name }}
14 | {{- include "youtube-live-chat-to-discord.selectorLabels" $ | nindent 6 }}
15 | template:
16 | metadata:
17 | labels:
18 | app: {{ .name }}
19 | {{- include "youtube-live-chat-to-discord.selectorLabels" $ | nindent 8 }}
20 | spec:
21 | restartPolicy: Always
22 | securityContext:
23 | runAsNonRoot: true
24 | containers:
25 | - name: {{ .name }}
26 | args:
27 | - {{ quote .youtubeId }}
28 | - {{ quote .discordWebhook }}
29 | env:
30 | - name: KUBERNETES_CLUSTER_DOMAIN
31 | value: {{ quote $.Values.kubernetesClusterDomain }}
32 | - name: Logging__LogLevel__Default
33 | value: Debug
34 | image: ghcr.io/jim60105/youtubelivechattodiscord:latest
35 | resources:
36 | limits:
37 | memory: "512Mi"
38 | cpu: "100m"
39 | requests:
40 | memory: "256Mi"
41 | cpu: "50m"
42 | securityContext:
43 | allowPrivilegeEscalation: false
44 | capabilities:
45 | drop: ["ALL"]
46 | seccompProfile:
47 | type: "RuntimeDefault"
48 | runAsUser: 1654
49 | runAsGroup: 1654
50 | {{- if .useCookies }}
51 | volumeMounts:
52 | - mountPath: /app/cookies.txt
53 | name: cookies
54 | subPath: cookies.txt
55 | volumes:
56 | - name: cookies
57 | configMap:
58 | name: {{ include "youtube-live-chat-to-discord.fullname" $ }}-cookies
59 | {{- end }}
60 | {{- end }}
--------------------------------------------------------------------------------
/helm-chart/values.yaml:
--------------------------------------------------------------------------------
1 | deployments:
2 | - name: demo1
3 | youtubeId: dHT1kFn96G0
4 | discordWebhook: https://discord.com/api/webhooks/9000000000000/OOXXOOXX
5 | useCookies: false
6 |
7 | cookies: |
8 | # Netscape HTTP Cookie File
9 | # http://curl.haxx.se/rfc/cookie_spec.html
10 | # This is a generated file! Do not edit.
11 | .youtube.com TRUE / FALSE 1703311569 HSID AAAABBBBCCCCSSSSS
12 | .youtube.com TRUE / TRUE 1703311569 SSID HHHHJJJJJKKKKLLLL
13 |
14 | kubernetesClusterDomain: cluster.local
--------------------------------------------------------------------------------