diff --git a/KfChatDotNetBot/Models/DbModels/StreamDbModel.cs b/KfChatDotNetBot/Models/DbModels/StreamDbModel.cs index 2273436..fc96116 100644 --- a/KfChatDotNetBot/Models/DbModels/StreamDbModel.cs +++ b/KfChatDotNetBot/Models/DbModels/StreamDbModel.cs @@ -33,12 +33,35 @@ public enum StreamService KiwiPeerTube } -public class KickStreamMetaModel +public class BaseMetaModel +{ + public CaptureOverridesModel? CaptureOverrides { get; set; } = null; +} + +public class CaptureOverridesModel +{ + // Options applicable to YtDlp will not work with Streamlink and vice versa + // That being said, some options are shared while still being explicitly marked as YtDlp + // This applies to CaptureYtDlpWorkingDirectory, CaptureYtDlpParentTerminal, and CaptureYtDlpScriptPath + public string? CaptureYtDlpBinaryPath { get; set; } = null; + public string? CaptureYtDlpWorkingDirectory { get; set; } = null; + public string? CaptureYtDlpCookiesFromBrowser { get; set; } = null; + public string? CaptureYtDlpOutputFormat { get; set; } = null; + public string? CaptureYtDlpParentTerminal { get; set; } = null; + public string? CaptureYtDlpScriptPath { get; set; } = null; + public string? CaptureYtDlpUserAgent { get; set; } = null; + public string? CaptureStreamlinkBinaryPath { get; set; } = null; + public string? CaptureStreamlinkOutputFormat { get; set; } = null; + public string? CaptureStreamlinkRemuxScript { get; set; } = null; + public string? CaptureStreamlinkTwitchOptions { get; set; } = null; +} + +public class KickStreamMetaModel : BaseMetaModel { public required int ChannelId { get; set; } } -public class PeerTubeMetaModel +public class PeerTubeMetaModel : BaseMetaModel { public required string AccountName { get; set; } } \ No newline at end of file diff --git a/KfChatDotNetBot/Services/BotServices.cs b/KfChatDotNetBot/Services/BotServices.cs index 55635d9..25e5d68 100644 --- a/KfChatDotNetBot/Services/BotServices.cs +++ b/KfChatDotNetBot/Services/BotServices.cs @@ -920,7 +920,7 @@ public class BotServices { _logger.Info($"BossmanJack stream event came in. isLive => {isLive}"); var settings = SettingsProvider.GetMultipleValuesAsync([ - BuiltIn.Keys.TwitchBossmanJackUsername, BuiltIn.Keys.CaptureEnabled, BuiltIn.Keys.TwitchIcon + BuiltIn.Keys.TwitchBossmanJackUsername, BuiltIn.Keys.CaptureEnabled, BuiltIn.Keys.TwitchIcon, BuiltIn.Keys.CaptureStreamlinkBmjWorkingDirectory ]).Result; var bmjUsername = settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value; @@ -930,7 +930,12 @@ public class BotServices if (settings[BuiltIn.Keys.CaptureEnabled].ToBoolean()) { _logger.Info("Capturing Bossman's stream"); - _ = new StreamCapture($"https://www.twitch.tv/{settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value}", StreamCaptureMethods.Streamlink, _cancellationToken).CaptureAsync(); + _ = new StreamCapture($"https://www.twitch.tv/{settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value}", + StreamCaptureMethods.Streamlink, + new CaptureOverridesModel + { + CaptureYtDlpWorkingDirectory = settings[BuiltIn.Keys.CaptureStreamlinkBmjWorkingDirectory].Value + }, _cancellationToken).CaptureAsync(); } return; } @@ -1025,6 +1030,7 @@ public class BotServices using var db = new ApplicationDbContext(); var channels = db.Streams.Where(s => s.Service == StreamService.Kick).Include(s => s.User); StreamDbModel? channel = null; + KickStreamMetaModel? meta = null; foreach (var ch in channels) { if (ch.Metadata == null) @@ -1033,7 +1039,6 @@ public class BotServices continue; } - KickStreamMetaModel meta; try { meta = JsonSerializer.Deserialize(ch.Metadata) ?? @@ -1071,7 +1076,7 @@ public class BotServices if (channel.AutoCapture && settings[BuiltIn.Keys.CaptureEnabled].ToBoolean()) { _logger.Info($"{channel.StreamUrl} is configured to auto capture"); - _ = new StreamCapture(channel.StreamUrl, StreamCaptureMethods.YtDlp, _cancellationToken).CaptureAsync(); + _ = new StreamCapture(channel.StreamUrl, StreamCaptureMethods.YtDlp, meta?.CaptureOverrides, _cancellationToken).CaptureAsync(); } } @@ -1098,12 +1103,26 @@ public class BotServices { identity = "@" + channel.User.KfUsername; } + + BaseMetaModel? meta = null; + if (channel.Metadata != null) + { + try + { + meta = JsonSerializer.Deserialize(channel.Metadata); + } + catch (Exception e) + { + _logger.Error($"Failed to deserialize metadata for Parti stream: {channel.StreamUrl}"); + _logger.Error(e); + } + } _chatBot.SendChatMessage($"{identity} is live! {data.EventTitle} {url}", true); if (channel.AutoCapture && settings[BuiltIn.Keys.CaptureEnabled].ToBoolean()) { _logger.Info($"{channel.StreamUrl} is configured to auto capture"); - _ = new StreamCapture(url, StreamCaptureMethods.YtDlp, _cancellationToken).CaptureAsync(); + _ = new StreamCapture(url, StreamCaptureMethods.YtDlp, meta?.CaptureOverrides, _cancellationToken).CaptureAsync(); } } @@ -1158,7 +1177,6 @@ public class BotServices $"{identity} is no longer live! :lossmanjack:", true); } - // TODO: Fix this so it aligns with the new Persisted Live setting instead of tracking separately public async Task CheckBmjIsLive() { var isLive = diff --git a/KfChatDotNetBot/Services/DLive.cs b/KfChatDotNetBot/Services/DLive.cs index 65214f7..b2121e7 100644 --- a/KfChatDotNetBot/Services/DLive.cs +++ b/KfChatDotNetBot/Services/DLive.cs @@ -72,10 +72,24 @@ public class DLive(ChatBot kfChatBot) : IDisposable await kfChatBot.SendChatMessageAsync($"{identity} is live! {status.Title} {stream.StreamUrl}", true); + BaseMetaModel? meta = null; + if (stream.Metadata != null) + { + try + { + meta = JsonSerializer.Deserialize(stream.Metadata); + } + catch (Exception e) + { + _logger.Error($"Caught an exception when attempting to deserialize metadata for DLive stream {stream.StreamUrl}"); + _logger.Error(e); + } + } + if (stream.AutoCapture && settings[BuiltIn.Keys.CaptureEnabled].ToBoolean()) { _logger.Info($"{stream.StreamUrl} is live and set to auto capture"); - _ = new StreamCapture(stream.StreamUrl, StreamCaptureMethods.Streamlink, ct).CaptureAsync(); + _ = new StreamCapture(stream.StreamUrl, StreamCaptureMethods.Streamlink, meta?.CaptureOverrides, ct).CaptureAsync(); } currentlyLive.Add(username); } diff --git a/KfChatDotNetBot/Services/Owncast.cs b/KfChatDotNetBot/Services/Owncast.cs index a8b78eb..86ba632 100644 --- a/KfChatDotNetBot/Services/Owncast.cs +++ b/KfChatDotNetBot/Services/Owncast.cs @@ -47,7 +47,7 @@ public class Owncast(ChatBot kfChatBot) : IDisposable if (!status.Online) continue; await kfChatBot.SendChatMessageAsync("https://bossmanjack.tv restream is live!", true); if (!(await SettingsProvider.GetValueAsync(BuiltIn.Keys.CaptureEnabled)).ToBoolean()) continue; - _ = new StreamCapture("https://bossmanjack.tv", StreamCaptureMethods.YtDlp, ct).CaptureAsync(); + _ = new StreamCapture("https://bossmanjack.tv", StreamCaptureMethods.YtDlp, null, ct).CaptureAsync(); } } diff --git a/KfChatDotNetBot/Services/PeerTube.cs b/KfChatDotNetBot/Services/PeerTube.cs index 675e37d..76730ca 100644 --- a/KfChatDotNetBot/Services/PeerTube.cs +++ b/KfChatDotNetBot/Services/PeerTube.cs @@ -57,6 +57,7 @@ public class PeerTube(ChatBot kfChatBot) : IDisposable { if (persistedLive.Contains(stream.Uuid)) continue; StreamDbModel? dbEntry = null; + PeerTubeMetaModel? meta = null; foreach (var row in streams) { if (row.Metadata == null) @@ -64,13 +65,16 @@ public class PeerTube(ChatBot kfChatBot) : IDisposable _logger.Error($"Stream ID {row.Id} has null metadata"); continue; } - var meta = JsonSerializer.Deserialize(row.Metadata); + meta = JsonSerializer.Deserialize(row.Metadata); if (meta == null) { _logger.Error($"Caught a null when deserializing the metadata for {row.Id}"); continue; } - if (meta.AccountName == stream.Account.Name) dbEntry = row; + + if (meta.AccountName != stream.Account.Name) continue; + dbEntry = row; + break; } if (settings[BuiltIn.Keys.KiwiPeerTubeEnforceWhitelist].ToBoolean() && dbEntry == null) { @@ -89,7 +93,7 @@ public class PeerTube(ChatBot kfChatBot) : IDisposable continue; } _logger.Info($"{stream.Url} is live and set to auto capture (if configured)"); - _ = new StreamCapture(stream.Url, StreamCaptureMethods.YtDlp, ct).CaptureAsync(); + _ = new StreamCapture(stream.Url, StreamCaptureMethods.YtDlp, meta?.CaptureOverrides, ct).CaptureAsync(); } } diff --git a/KfChatDotNetBot/Services/StreamCapture.cs b/KfChatDotNetBot/Services/StreamCapture.cs index 0fbfdc0..ccbc66d 100644 --- a/KfChatDotNetBot/Services/StreamCapture.cs +++ b/KfChatDotNetBot/Services/StreamCapture.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Settings; using NLog; @@ -9,7 +10,7 @@ namespace KfChatDotNetBot.Services; /// /// Streamer URL /// Cancellation token -public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod, CancellationToken ct = default) +public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod, CaptureOverridesModel? captureOverrides = null, CancellationToken ct = default) { private readonly Dictionary _settings = SettingsProvider .GetMultipleValuesAsync([BuiltIn.Keys.CaptureYtDlpBinaryPath, BuiltIn.Keys.CaptureYtDlpWorkingDirectory, @@ -38,7 +39,7 @@ public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod, } else if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD()) { - pStartInfoFileName = _settings[BuiltIn.Keys.CaptureYtDlpParentTerminal].Value!; + pStartInfoFileName = captureOverrides?.CaptureYtDlpParentTerminal ?? _settings[BuiltIn.Keys.CaptureYtDlpParentTerminal].Value!; pStartInfoExecuteArgument = "-x"; pStartInfoExecuteScript = scriptPath; } @@ -51,7 +52,7 @@ public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod, { FileName = pStartInfoFileName, ArgumentList = { pStartInfoExecuteArgument, pStartInfoExecuteScript }, - WorkingDirectory = _settings[BuiltIn.Keys.CaptureYtDlpWorkingDirectory].Value + WorkingDirectory = captureOverrides?.CaptureYtDlpWorkingDirectory ?? _settings[BuiltIn.Keys.CaptureYtDlpWorkingDirectory].Value }); if (process == null) @@ -94,7 +95,7 @@ public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod, private async Task CreateScriptAsync() { var random = Convert.ToHexString(Guid.NewGuid().ToByteArray()[..4]); - var scriptPath = Path.Join(_settings[BuiltIn.Keys.CaptureYtDlpScriptPath].Value, + var scriptPath = Path.Join(_settings[captureOverrides?.CaptureYtDlpScriptPath ?? BuiltIn.Keys.CaptureYtDlpScriptPath].Value, $"bot_ytdlp_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{random}.sh"); if (OperatingSystem.IsWindows()) { @@ -105,9 +106,10 @@ public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod, string captureLine; if (captureMethod == StreamCaptureMethods.YtDlp) { - captureLine = $"{_settings[BuiltIn.Keys.CaptureYtDlpBinaryPath].Value} -o \"{_settings[BuiltIn.Keys.CaptureYtDlpOutputFormat].Value}\" " + - $"--user-agent \"{_settings[BuiltIn.Keys.CaptureYtDlpUserAgent].Value}\" " + - $"--cookies-from-browser {_settings[BuiltIn.Keys.CaptureYtDlpCookiesFromBrowser].Value} " + + captureLine = $"{captureOverrides?.CaptureYtDlpBinaryPath ?? _settings[BuiltIn.Keys.CaptureYtDlpBinaryPath].Value} " + + $"-o \"{captureOverrides?.CaptureYtDlpOutputFormat ?? _settings[BuiltIn.Keys.CaptureYtDlpOutputFormat].Value}\" " + + $"--user-agent \"{captureOverrides?.CaptureYtDlpUserAgent ?? _settings[BuiltIn.Keys.CaptureYtDlpUserAgent].Value}\" " + + $"--cookies-from-browser {captureOverrides?.CaptureYtDlpCookiesFromBrowser ?? _settings[BuiltIn.Keys.CaptureYtDlpCookiesFromBrowser].Value} " + $"--write-info-json --wait-for-video 15 --merge-output-format mp4 --verbose {streamUrl}"; } else if (captureMethod == StreamCaptureMethods.Streamlink) @@ -115,9 +117,10 @@ public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod, var twitchOpts = string.Empty; if (streamUrl.Contains("twitch.tv")) { - twitchOpts = _settings[BuiltIn.Keys.CaptureStreamlinkTwitchOptions].Value; + twitchOpts = captureOverrides?.CaptureStreamlinkTwitchOptions ?? _settings[BuiltIn.Keys.CaptureStreamlinkTwitchOptions].Value; } - captureLine = $"{_settings[BuiltIn.Keys.CaptureStreamlinkBinaryPath].Value} {twitchOpts} --output \"{_settings[BuiltIn.Keys.CaptureStreamlinkOutputFormat].Value}\" " + + captureLine = $"{captureOverrides?.CaptureStreamlinkBinaryPath ?? _settings[BuiltIn.Keys.CaptureStreamlinkBinaryPath].Value} {twitchOpts} " + + $"--output \"{captureOverrides?.CaptureStreamlinkOutputFormat ?? _settings[BuiltIn.Keys.CaptureStreamlinkOutputFormat].Value}\" " + $"--retry-streams 15 --retry-max 10 {streamUrl} best"; } else @@ -129,7 +132,7 @@ public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod, var remuxLine = string.Empty; if (captureMethod == StreamCaptureMethods.Streamlink) { - remuxLine = _settings[BuiltIn.Keys.CaptureStreamlinkRemuxScript].Value; + remuxLine = captureOverrides?.CaptureStreamlinkRemuxScript ?? _settings[BuiltIn.Keys.CaptureStreamlinkRemuxScript].Value; } string scriptContent; @@ -138,8 +141,8 @@ public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod, { // GetPathRoot on Windows returns the top level directory, e.g. "C:\". Assuming the working directory is on another drive such as D: // we'll need to swap to that drive letter, so this just trims off the \ to transform it to D: or whatever. UNC paths not supported - scriptContent = $"{Path.GetPathRoot(_settings[BuiltIn.Keys.CaptureYtDlpWorkingDirectory].Value)?.TrimEnd('\\')}{Environment.NewLine}" + - $"CD {_settings[BuiltIn.Keys.CaptureYtDlpWorkingDirectory].Value}{Environment.NewLine}" + + scriptContent = $"{Path.GetPathRoot(captureOverrides?.CaptureYtDlpWorkingDirectory ?? _settings[BuiltIn.Keys.CaptureYtDlpWorkingDirectory].Value)?.TrimEnd('\\')}{Environment.NewLine}" + + $"CD {captureOverrides?.CaptureYtDlpWorkingDirectory ?? _settings[BuiltIn.Keys.CaptureYtDlpWorkingDirectory].Value}{Environment.NewLine}" + $"{captureLine}{Environment.NewLine}" + $"{remuxLine}{Environment.NewLine}" + $"PAUSE"; @@ -147,7 +150,7 @@ public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod, else if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD()) { scriptContent = $"#!/bin/bash{Environment.NewLine}" + - $"cd {_settings[BuiltIn.Keys.CaptureYtDlpWorkingDirectory].Value}{Environment.NewLine}" + + $"cd {captureOverrides?.CaptureYtDlpWorkingDirectory ?? _settings[BuiltIn.Keys.CaptureYtDlpWorkingDirectory].Value}{Environment.NewLine}" + $"{captureLine}{Environment.NewLine}" + $"{remuxLine}{Environment.NewLine}" + $"read -p \"Press enter to exit\""; diff --git a/KfChatDotNetBot/Settings/BuiltIn.cs b/KfChatDotNetBot/Settings/BuiltIn.cs index 32c8fb4..00a3fee 100644 --- a/KfChatDotNetBot/Settings/BuiltIn.cs +++ b/KfChatDotNetBot/Settings/BuiltIn.cs @@ -1061,6 +1061,13 @@ public static class BuiltIn Default = "300", ValueType = SettingValueType.Text, Regex = WholeNumberRegex + }, + new BuiltInSettingsModel + { + Key = Keys.CaptureStreamlinkBmjWorkingDirectory, + Description = "Working directory for BMJ's Twitch streams captured with streamlink", + Default = "/root/twitch/", + ValueType = SettingValueType.Text } ]; @@ -1183,5 +1190,6 @@ public static class BuiltIn public static string BotImageChinkSelfDestructDelay = "Bot.Image.ChinkSelfDestructDelay"; public static string BotRateLimitCooldownAutoDeleteDelay = "Bot.RateLimit.CooldownAutoDeleteDelay"; public static string BotRateLimitExpiredEntryCleanupInterval = "Bot.RateLimit.ExpiredEntryCleanupInterval"; + public static string CaptureStreamlinkBmjWorkingDirectory = "Bot.Streamlink.BmjWorkingDirectory"; } } \ No newline at end of file