YouTube PubSub

This commit is contained in:
barelyprofessional
2025-12-19 00:49:18 -06:00
parent 72a162b67a
commit a88045c63d
5 changed files with 137 additions and 13 deletions

View File

@@ -0,0 +1,41 @@
using System.Text.Json.Serialization;
namespace KfChatDotNetBot.Models;
public class YouTubeApiModels
{
public class ContentDetailsRoot
{
[JsonPropertyName("kind")]
public required string Kind { get; set; }
[JsonPropertyName("items")]
public required List<ItemModel> Items { get; set; }
}
public class SnippetModel
{
[JsonPropertyName("publishedAt")]
public required DateTime PublishedAt { get; set; }
[JsonPropertyName("channelId")]
public required string ChannelId { get; set; }
[JsonPropertyName("title")]
public required string Title { get; set; }
[JsonPropertyName("description")]
public required string Description { get; set; }
[JsonPropertyName("channelTitle")]
public required string ChannelTitle { get; set; }
// "none", "live", "upcoming"
[JsonPropertyName("liveBroadcastContent")]
public required string LiveBroadcastContent { get; set; }
}
public class ItemModel
{
[JsonPropertyName("kind")]
public required string Kind { get; set; }
[JsonPropertyName("id")]
public required string Id { get; set; }
[JsonPropertyName("snippet")]
public required SnippetModel Snippet { get; set; }
}
}

View File

@@ -39,6 +39,7 @@ public class BotServices
private PeerTube? _peerTubeStatusCheck;
private Owncast? _owncastStatusCheck;
private ShuffleDotUs? _shuffleDotUs;
private YouTubePubSub? _youTubePubSub;
public OpenRouter? OpenRouter;
private Task? _websocketWatchdog;
@@ -130,6 +131,20 @@ public class BotServices
_shuffleDotUs.OnLatestBetUpdated += ShuffleOnLatestBetUpdated;
await _shuffleDotUs.StartWsClient();
}
private async Task BuildYouTubePubSub()
{
var settings = await SettingsProvider.GetMultipleValuesAsync([BuiltIn.Keys.YouTubePubSubEnabled]);
if (!settings[BuiltIn.Keys.YouTubePubSubEnabled].ToBoolean())
{
_logger.Debug("YouTube PubSub is disabled");
return;
}
_youTubePubSub = new YouTubePubSub(_cancellationToken);
_youTubePubSub.OnNewVideo += YouTubePubSubOnNewVideo;
await _youTubePubSub.Connect();
_logger.Info("Built YouTube PubSub");
}
private async Task BuildDiscord()
{
@@ -393,7 +408,8 @@ public class BotServices
var settings = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KickEnabled, BuiltIn.Keys.HowlggEnabled, BuiltIn.Keys.ChipsggEnabled,
BuiltIn.Keys.ClashggEnabled, BuiltIn.Keys.BetBoltEnabled, BuiltIn.Keys.YeetEnabled,
BuiltIn.Keys.RainbetEnabled, BuiltIn.Keys.PartiEnabled, BuiltIn.Keys.JackpotEnabled
BuiltIn.Keys.RainbetEnabled, BuiltIn.Keys.PartiEnabled, BuiltIn.Keys.JackpotEnabled,
BuiltIn.Keys.YouTubePubSubEnabled
]);
try
{
@@ -508,6 +524,15 @@ public class BotServices
_shuffleDotUs = null!;
await BuildShuffleDotUs();
}
if (settings[BuiltIn.Keys.YouTubePubSubEnabled].ToBoolean() && _youTubePubSub != null &&
!_youTubePubSub.IsConnected())
{
_logger.Error("YouTube PubSub died, recreating it");
_youTubePubSub.Dispose();
_youTubePubSub = null;
await BuildYouTubePubSub();
}
}
catch (Exception e)
{
@@ -1253,4 +1278,31 @@ public class BotServices
};
await SettingsProvider.SetValueAsJsonObjectAsync(BuiltIn.Keys.BossmanLastSighting, sighting);
}
private void YouTubePubSubOnNewVideo(object sender, YouTubePubSubNotificationModel data)
{
var video = YouTubeApi.GetVideoDetails(data.Id).Result;
if (video == null)
{
_logger.Error($"Caught a null when getting video details for {data.Id} which we were just notified about");
return;
}
if (video.Snippet.LiveBroadcastContent == "live")
{
_chatBot.SendChatMessageAsync($"@{video.Snippet.ChannelTitle} has gone live! {data.Title} {data.Url}", true).Wait(_cancellationToken);
}
else if (video.Snippet.LiveBroadcastContent == "upcoming")
{
_chatBot.SendChatMessageAsync($"@{video.Snippet.ChannelTitle} has scheduled a live stream! {data.Title} {data.Url}", true).Wait(_cancellationToken);
}
else if (video.Snippet.LiveBroadcastContent == "none")
{
_chatBot.SendChatMessageAsync($"@{video.Snippet.ChannelTitle} has uploaded a new video! {data.Title} {data.Url}", true).Wait(_cancellationToken);
}
else
{
_logger.Error($"YouTube live broadcast content '{video.Snippet.LiveBroadcastContent}' was unhandled for {data.Id}");
}
}
}

View File

@@ -1,6 +1,42 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Settings;
namespace KfChatDotNetBot.Services;
public class YouTubeApi
public static class YouTubeApi
{
private static async Task<HttpClient> GetHttpClient()
{
var settings =
await SettingsProvider.GetMultipleValuesAsync([BuiltIn.Keys.YouTubeApiKey, BuiltIn.Keys.Proxy]);
if (settings[BuiltIn.Keys.YouTubeApiKey].Value == null)
throw new InvalidOperationException("YouTube API key has not been configured");
var handler = new HttpClientHandler();
if (settings[BuiltIn.Keys.Proxy].Value != null)
{
handler.Proxy = new WebProxy(settings[BuiltIn.Keys.Proxy].Value);
handler.UseProxy = true;
}
handler.AutomaticDecompression = DecompressionMethods.All;
var client = new HttpClient(handler);
client.DefaultRequestHeaders.UserAgent.Clear();
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("KenoGPT", "1.0"));
client.DefaultRequestHeaders.Add("X-Goog-Api-Key", settings[BuiltIn.Keys.YouTubeApiKey].Value);
client.BaseAddress = new Uri("https://www.googleapis.com/youtube/v3/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return client;
}
public static async Task<YouTubeApiModels.ItemModel?> GetVideoDetails(string id)
{
var client = await GetHttpClient();
var response =
await client.GetFromJsonAsync<YouTubeApiModels.ContentDetailsRoot>($"videos?part=snippet&id={id}");
if (response?.Items.Count == 0) return null;
return response?.Items[0];
}
}

View File

@@ -60,6 +60,7 @@ public class YouTubePubSub : IDisposable
_logger.Error(e);
_logger.Error("--- Payload ---");
_logger.Error(message.ToString());
_logger.Error("---------------");
}
}
@@ -79,14 +80,4 @@ public class YouTubePubSubNotificationModel
public required string Title { get; set; }
[JsonPropertyName("url")]
public required string Url { get; set; }
[JsonPropertyName("channel")]
public required YouTubePubSubNotificationChannelModel Channel { get; set; }
}
public class YouTubePubSubNotificationChannelModel
{
[JsonPropertyName("id")]
public required string Id { get; set; }
[JsonPropertyName("name")]
public required string Name { get; set; }
}

View File

@@ -453,6 +453,10 @@ public static class BuiltIn
public static string KasinoDiceCleanupDelay = "Kasino.Dice.CleanupDelay";
[BuiltInSetting("Delay in milliseconds before cleaning up wheel", SettingValueType.Text, "30000", WholeNumberRegex)]
public static string KasinoWheelCleanupDelay = "Kasino.Wheel.CleanupDelay";
[BuiltInSetting("Whether the YouTube PubSub Redis client is enabled", SettingValueType.Boolean, "true", BooleanRegex)]
public static string YouTubePubSubEnabled = "YouTube.PubSub.Enabled";
[BuiltInSetting("YouTube API Key", SettingValueType.Text, isSecret: true)]
public static string YouTubeApiKey = "YouTube.ApiKey";
[BuiltInSetting("Openrouter API key for hostess command", SettingValueType.Text, isSecret: true)]
public static string OpenrouterApiKey = "Openrouter.ApiKey";
}