Added support for selectively overriding capture settings on a per-stream basis

This commit is contained in:
barelyprofessional
2025-09-14 01:05:37 -05:00
parent d76f427621
commit 15de60e60b
7 changed files with 96 additions and 26 deletions

View File

@@ -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; }
}

View File

@@ -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<KickStreamMetaModel>(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<BaseMetaModel>(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<bool> CheckBmjIsLive()
{
var isLive =

View File

@@ -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<BaseMetaModel>(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);
}

View File

@@ -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();
}
}

View File

@@ -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<PeerTubeMetaModel>(row.Metadata);
meta = JsonSerializer.Deserialize<PeerTubeMetaModel>(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();
}
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Settings;
using NLog;
@@ -9,7 +10,7 @@ namespace KfChatDotNetBot.Services;
/// </summary>
/// <param name="streamUrl">Streamer URL</param>
/// <param name="ct">Cancellation token</param>
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<string, Setting> _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<string> 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\"";

View File

@@ -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";
}
}