feat: expand platform adapters and preview tooling

This commit is contained in:
2026-05-13 19:40:43 +08:00
parent b81ead700d
commit 7a286fd619
44 changed files with 4370 additions and 115 deletions
@@ -28,7 +28,10 @@ public sealed class EmailNotificationService : IEmailNotificationService
_logger = logger;
}
public async Task SendLiveStartedAsync(LiveRoom liveRoom, CancellationToken cancellationToken = default)
public async Task SendLiveStartedAsync(
LiveRoom liveRoom,
CancellationToken cancellationToken = default,
string? eventScriptOutput = null)
{
var settings = await _systemSettingsService.GetAsync(cancellationToken);
if (!settings.EnableEmailNotification || !settings.NotifyOnLiveStarted)
@@ -43,7 +46,8 @@ public sealed class EmailNotificationService : IEmailNotificationService
["title"] = liveRoom.Title,
["anchor"] = liveRoom.AnchorName,
["sourceUrl"] = liveRoom.SourceUrl,
["detectedAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O")
["detectedAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O"),
["eventScriptOutput"] = NormalizeNotificationText(eventScriptOutput)
});
var subject = RenderSubject(settings.EmailLiveStartedSubjectTemplate, tokens);
@@ -58,7 +62,8 @@ public sealed class EmailNotificationService : IEmailNotificationService
string? detail = null,
LiveRoom? liveRoom = null,
RecordTask? recordTask = null,
CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default,
string? eventScriptOutput = null)
{
var settings = await _systemSettingsService.GetAsync(cancellationToken);
if (!settings.EnableEmailNotification || !settings.NotifyOnException)
@@ -75,7 +80,8 @@ public sealed class EmailNotificationService : IEmailNotificationService
["roomId"] = liveRoom?.RoomId,
["recordTaskId"] = recordTask?.Id.ToString(),
["taskStatus"] = recordTask?.Status.ToString(),
["occurredAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O")
["occurredAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O"),
["eventScriptOutput"] = NormalizeNotificationText(eventScriptOutput)
});
var subject = RenderSubject(settings.EmailExceptionSubjectTemplate, tokens);
@@ -114,7 +120,8 @@ public sealed class EmailNotificationService : IEmailNotificationService
["title"] = "Sample Live Title",
["anchor"] = "Sample Anchor",
["sourceUrl"] = "https://live.douyin.com/123456789",
["detectedAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O")
["detectedAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O"),
["eventScriptOutput"] = "line one from script\nline two from script"
});
var sampleExceptionTokens = CreateTokenMap(new Dictionary<string, string?>
{
@@ -125,7 +132,8 @@ public sealed class EmailNotificationService : IEmailNotificationService
["roomId"] = "123456789",
["recordTaskId"] = Guid.NewGuid().ToString(),
["taskStatus"] = "Running",
["occurredAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O")
["occurredAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O"),
["eventScriptOutput"] = "line one from script\nline two from script"
});
var body = $$"""
@@ -275,6 +283,24 @@ public sealed class EmailNotificationService : IEmailNotificationService
return tokens;
}
private static string NormalizeNotificationText(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
const int maxLength = 8000;
const string suffix = "... [truncated]";
var trimmed = value.Trim();
if (trimmed.Length <= maxLength)
{
return trimmed;
}
return string.Concat(trimmed[..(maxLength - suffix.Length)], suffix);
}
private static string RenderSubject(string template, IReadOnlyDictionary<string, string> tokens)
{
var rendered = RenderTemplate(template, tokens, htmlEncodeValues: false);
@@ -30,12 +30,15 @@ public sealed class EventScriptService : IEventScriptService
_logger = logger;
}
public async Task RunLiveStartedAsync(LiveRoom liveRoom, DateTimeOffset occurredAt, CancellationToken cancellationToken = default)
public async Task<EventScriptExecutionResultDto?> RunLiveStartedAsync(
LiveRoom liveRoom,
DateTimeOffset occurredAt,
CancellationToken cancellationToken = default)
{
var settings = await _settingsService.GetAsync(cancellationToken);
var environment = BuildLiveRoomEnvironment(liveRoom, occurredAt);
environment["LIVE_RECORDER_EVENT"] = "live_started";
await RunAsync(
return await RunAsync(
settings.EnableEventScripts && settings.EnableLiveStartedScript,
settings.LiveStartedScriptMode,
settings.LiveStartedScriptPath,
@@ -50,12 +53,15 @@ public sealed class EventScriptService : IEventScriptService
cancellationToken);
}
public async Task RunLiveEndedAsync(LiveRoom liveRoom, DateTimeOffset occurredAt, CancellationToken cancellationToken = default)
public async Task<EventScriptExecutionResultDto?> RunLiveEndedAsync(
LiveRoom liveRoom,
DateTimeOffset occurredAt,
CancellationToken cancellationToken = default)
{
var settings = await _settingsService.GetAsync(cancellationToken);
var environment = BuildLiveRoomEnvironment(liveRoom, occurredAt);
environment["LIVE_RECORDER_EVENT"] = "live_ended";
await RunAsync(
return await RunAsync(
settings.EnableEventScripts && settings.EnableLiveEndedScript,
settings.LiveEndedScriptMode,
settings.LiveEndedScriptPath,
@@ -70,7 +76,7 @@ public sealed class EventScriptService : IEventScriptService
cancellationToken);
}
public async Task RunSegmentCompletedAsync(
public async Task<EventScriptExecutionResultDto?> RunSegmentCompletedAsync(
LiveRoom? liveRoom,
RecordSession recordSession,
RecordTask recordTask,
@@ -93,7 +99,7 @@ public sealed class EventScriptService : IEventScriptService
environment["LIVE_RECORDER_TASK_STATUS"] = recordTask.Status.ToString();
environment["LIVE_RECORDER_SESSION_STATUS"] = recordSession.Status.ToString();
await RunAsync(
return await RunAsync(
forceRun || (settings.EnableEventScripts && settings.EnableSegmentCompletedScript),
settings.SegmentCompletedScriptMode,
settings.SegmentCompletedScriptPath,
@@ -156,7 +162,15 @@ public sealed class EventScriptService : IEventScriptService
};
}
private async Task RunAsync(
private static EventScriptExecutionResultDto MapOutcome(ScriptExecutionOutcome outcome) => new()
{
Success = outcome.Success,
Message = outcome.Message,
Detail = outcome.Detail,
CustomLogOutput = outcome.CustomLogOutput
};
private async Task<EventScriptExecutionResultDto?> RunAsync(
bool enabled,
string scriptMode,
string scriptPath,
@@ -172,10 +186,10 @@ public sealed class EventScriptService : IEventScriptService
{
if (!enabled)
{
return;
return null;
}
await ExecuteAsync(
var outcome = await ExecuteAsync(
scriptMode,
scriptPath,
scriptContent,
@@ -187,6 +201,8 @@ public sealed class EventScriptService : IEventScriptService
recordSessionId,
recordTaskId,
cancellationToken);
return MapOutcome(outcome);
}
private async Task<ScriptExecutionOutcome> ExecuteAsync(
@@ -1401,7 +1401,20 @@ public sealed partial class FfmpegService
}
includeNonChatEvents = runtime.RecordingSettings.DanmakuIncludeNonChatEvents;
var adapter = adapterFactory.GetByPlatform(liveRoom.Platform);
var adapter = adapterFactory.TryGetByPlatform(liveRoom.Platform);
if (adapter is null)
{
await WriteDanmakuSystemLogAsync(
SystemLogLevel.Info,
$"No danmaku adapter is registered for {liveRoom.Platform}. Recording will continue without live comments.",
null,
runtime.LiveRoomId,
runtime.RecordSessionId,
initialTask.Id,
cancellationToken);
return;
}
runtime.DanmakuConnection = await adapter.ConnectAsync(
new DanmakuConnectionContext(
liveRoom.Id,
@@ -1,6 +1,7 @@
using System.Net;
using System.Net.Security;
using System.Security.Authentication;
using LiveRecorder.Application.Common;
using LiveRecorder.Application.Abstractions.Settings;
using LiveRecorder.Application.Models.Settings;
using LiveRecorder.Domain.Enums;
@@ -54,15 +55,14 @@ public sealed class PlatformHttpClientFactory
private static IWebProxy? BuildProxy(LivePlatformType platform, SystemSettingsDto settings)
{
var proxySettings = platform switch
if (!LivePlatformCatalog.TryGet(platform, out _))
{
LivePlatformType.Douyin => settings.DouyinProxy,
LivePlatformType.Bilibili => settings.BilibiliProxy,
LivePlatformType.Huya => settings.HuyaProxy,
_ => null
};
return null;
}
if (proxySettings is null || !proxySettings.Enabled || string.IsNullOrWhiteSpace(proxySettings.ProxyUrl))
var proxySettings = settings.GetPlatformRequestSettings(platform).Proxy;
if (!proxySettings.Enabled || string.IsNullOrWhiteSpace(proxySettings.ProxyUrl))
{
return null;
}
@@ -34,7 +34,10 @@ public sealed class WebhookNotificationService : IWebhookNotificationService
_logger = logger;
}
public async Task SendLiveStartedAsync(LiveRoom liveRoom, CancellationToken cancellationToken = default)
public async Task SendLiveStartedAsync(
LiveRoom liveRoom,
CancellationToken cancellationToken = default,
string? eventScriptOutput = null)
{
var settings = await _systemSettingsService.GetAsync(cancellationToken);
if (!settings.EnableWebhookNotification || !settings.NotifyWebhookOnLiveStarted)
@@ -49,7 +52,8 @@ public sealed class WebhookNotificationService : IWebhookNotificationService
source: "LiveRoomStatus",
liveRoom: liveRoom,
recordTask: null,
report: null);
report: null,
eventScriptOutput: eventScriptOutput);
var variables = BuildTemplateVariables(payload, report: null);
await SendInternalAsync(
@@ -66,7 +70,8 @@ public sealed class WebhookNotificationService : IWebhookNotificationService
string? detail = null,
LiveRoom? liveRoom = null,
RecordTask? recordTask = null,
CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default,
string? eventScriptOutput = null)
{
var settings = await _systemSettingsService.GetAsync(cancellationToken);
if (!settings.EnableWebhookNotification || !settings.NotifyWebhookOnException)
@@ -81,7 +86,8 @@ public sealed class WebhookNotificationService : IWebhookNotificationService
source,
liveRoom,
recordTask,
report: null);
report: null,
eventScriptOutput: eventScriptOutput);
var variables = BuildTemplateVariables(payload, report: null);
await SendInternalAsync(
@@ -111,7 +117,8 @@ public sealed class WebhookNotificationService : IWebhookNotificationService
"DailyReview",
liveRoom: null,
recordTask: null,
report);
report: report,
eventScriptOutput: null);
var variables = BuildTemplateVariables(payload, report);
await SendInternalAsync(
@@ -160,7 +167,8 @@ public sealed class WebhookNotificationService : IWebhookNotificationService
"SettingsTest",
sampleLiveRoom,
recordTask: null,
report: null);
report: null,
eventScriptOutput: "line one from script\nline two from script");
var variables = BuildTemplateVariables(payload, report: null);
try
@@ -339,7 +347,8 @@ public sealed class WebhookNotificationService : IWebhookNotificationService
string source,
LiveRoom? liveRoom,
RecordTask? recordTask,
DailyReviewReportDto? report)
DailyReviewReportDto? report,
string? eventScriptOutput)
{
var payload = new Dictionary<string, object?>
{
@@ -349,6 +358,7 @@ public sealed class WebhookNotificationService : IWebhookNotificationService
["summary"] = summary,
["detail"] = detail,
["source"] = source,
["eventScriptOutput"] = NormalizeNotificationText(eventScriptOutput),
["liveRoom"] = liveRoom is null ? null : new Dictionary<string, object?>
{
["id"] = liveRoom.Id,
@@ -403,6 +413,11 @@ public sealed class WebhookNotificationService : IWebhookNotificationService
["source"] = payload["source"]
};
if (payload.TryGetValue("eventScriptOutput", out var eventScriptOutput))
{
variables["eventScriptOutput"] = eventScriptOutput;
}
if (payload.TryGetValue("liveRoom", out var liveRoomPayload) &&
liveRoomPayload is IReadOnlyDictionary<string, object?> liveRoom)
{
@@ -436,6 +451,24 @@ public sealed class WebhookNotificationService : IWebhookNotificationService
return variables;
}
private static string NormalizeNotificationText(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
const int maxLength = 8000;
const string suffix = "... [truncated]";
var trimmed = value.Trim();
if (trimmed.Length <= maxLength)
{
return trimmed;
}
return string.Concat(trimmed[..(maxLength - suffix.Length)], suffix);
}
private static string Truncate(string? value, int maxLength)
{
if (string.IsNullOrWhiteSpace(value))